1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PythonでIdeone.comに登録するスクリプトを作成してみましょう。

Last updated at Posted at 2016-08-05

Python으로 Ideone.com에 업로드하는 스크립트를 만들어보자.

  • 未熟な日本語でポストがおかしく見えるかもしれません。
  • 先に謝ります。

初めに (개요)

コードの共有 (코드의 공유)

いろんなプログラミングコミュニティサイトを見ていると、
自分のコードを見せるために、他にもシンタックスハイライトを適用するために、
いろんなWeb IDEを使っているのを見ることができる。
いちいちHTMLタグでフォントと文字の色を書くことはできないからだ。

여러 프프로그래밍 커뮤니티를 둘러보다 보면,
자신의 소스 코드를 보여주기 위해, 혹은 Syntax Highlighting을 적용시키기 위해,
여러가지 Web IDE를 사용하는 것을 발견할 수 있다.
일일히 HTML 태그를 적어가며 폰트를 지정하고, 글자 색을 입힐 수는 없는 노릇이다.

いろんな Web IDEについて (여러 Web IDE에 대해)

codepadとか、IdeoneJSFiddle(HTML / CSS / Javascript 限定だけどもとにかく)、
Coding Ground(ここはWeb Shell基盤で利用できる)、
Pastebin.com(実行できなく見せるだけ)などたくさんあるが、
私にとってはideone.comの方が一番つかいやすかった。
サポートする言語も多いし、コードを実際に実行できるからだ。シンタックスハイライトの見かけが良いのはおまけ。

codepad, Ideone, JSFiddle(HTML / CSS / Javascript 밖에 못 쓰지만 아무튼),
Coding Ground(여기는 Web Shell 기반으로 이용 가능함),
Pastebin.com(실행은 부가능하고, 보여주기만 가능) 등 많이 있는데,
나는 ideone.com 쪽이 제일 쓰기 쉬웠던 것 같다.
지원하는 언어도 많을 뿐더러, 실제로 소스 코드를 실행해 볼 수도 있기 때문이다. Syntax Highlight 기능이 보기 좋게 꾸며주는 것은 덤.

だが、これもまた面倒くさい (허나, 이것마저도 귀찮다)

ウェブブラウザを探し、ideone.comとかに接続して、私のコードをコピペして、作られたURLをまたコピペ。それでこそリンクの共有ができる。
実に非効率的な動きだ。
だから、作成したところで続々アップロードしてすぐにURLをゲットしようにして見よう。

웹 브라우저로 가서, ideone.com 등으로 접속하고, 내 소스 코드를 복붙하여 실행 후, 만들어진 URL을 또 다시 복붙한다. 이제야 링크를 공유할 수 있다.
실로 비효율적인 움직임이다.
그러므로, 작성하고 바로 업로드해서 바로 URL을 얻을 수 있도록 해보자.

データの流れの分析 (데이터 흐름의 분석)

テストするためのシミュレーション (테스트를 위한 시뮬레이션)

ひとまず、実際にideone.comに接続してコードを登録して見よう。

우선 실제로 ideone.com 으로 접속해서 소스 코드를 등록해보자.

0001.png

簡単にtestだけ書いておく。
ここでRunボタンを押すと共有できるURLが出てくるが、今はそれが目的ではない。
アドレスが変わることを見るとFormタグとPOST伝送を利用する可能性が高い。
データを伝送する目的地のアドレスとデータを調べることが必要だ。
これのためにChromeの開発者道具を使ってみる。

간단하게 test만 적어둔다.
여기서 Run 버튼을 누르면 공유가 가능한 URL이 나올테지만, 지금 목적은 그게 아니다.
주소가 바뀐다는 사실을 보면 Form 태그로 POST 전송을 이용할 가능성이 높다.
데이터를 전송할 목적지의 주소와 데이터를 알아낼 필요가 있다.
이를 위해 Chrome의 개발자 도구를 사용한다.

0002.png

このようにページのローディングが全部終わったところでネットワークタブをクリアにして待機する。そしてRun
その後タイミングよくESCボタンで同作を止める。そうなると、

위와 같이 페이지 로딩이 완전히 끝난 상태에서 Network 탭을 비워둔다. 그리고 Run.
그 후 타이밍에 맞춰 ESC 버튼을 눌러 동작을 정지시킨다. 그러면,

結果 (결과)

0003.png

写真のようにPOST伝送したページの情報と、あと移動するため作られたページが現れる。あのsubmitページをクリックしてヘッダーの情報を見るとこう書いている。

사진처럼 POST 전송한 페이지의 정보와, 앞으로 이동할 만들어진 페이지가 나타난다. 이 submit 페이지를 클릭하여 헤더 정보를 보면 이렇게 적혀있다.

0004.png

/ideone/Index/submit/に接続するとHTTP 302を受けてLocationヘッダーでリダイレクトする。
ここから送ったデータも見てみよう。

/ideone/Index/submit/에 접속하면 HTTP 302를 받으며 Location 헤더를 통해 리다이렉션한다.
이쪽에서 보낸 데이터를 보자.

0005.png

いろんな変数が一緒に伝送された。そしてあのfile変数が先作成したコードになっているのを確認できる。
Locationヘッダーのアドレスを直接に入力して接続してみたら、

여러 변수가 함께 전송되었다. 그리고 file 변수에 아까 적은 소스 코드가 적혀있는 것을 확인할 수 있다.
Location 헤더의 주소를 직접 입력하여 접속해보면,

0006.png

確かに正しいページが作られている。

제대로 된 페이지가 확실히 만들어져있다.

整理 (정리)

ここでいったん整理してみよう。

여기까지 일단 정리해보자.

  • Runすると、/ideone/Index/submit/ページにPOSTでデータを送る。
  • (Run 하면 /ideone/Index/submit/ 페이지로 POST 방식으로 데이터를 보낸다.)
    • データ変数のリスト (데이터 변수 리스트)
      • p1, p2, p3, p4
      • clone_link
      • file: 作成したコード (작성한 소스 코드)
      • input: 標準入力に入るデータ (표준입력 데이터)
      • syntax: シンタックスハイライト (Syntax Highlight)
      • timelimit
      • note
      • _lang: プログラミング言語のコード (프로그래밍 언어 코드)
      • public
      • run: 実行するかどうか (실행 여부)
      • Submit
    • ボールドになっているのは重要そうな変数である。 (볼드 처리한 것은 다소 중요해보이는 변수)
  • サーバーからHTTP 302 Moved TemporarilyLocationヘッダーに結果物のURLが入っている。
  • (서버로부터 HTTP 302 Moved Temporarily 와 Location 헤더에 결과물의 URL이 들어가있다.)

どてもシンプルだ。
この情報を利用して今度は実際にスクリプトコードを書いてみる。

매우 심플하다.
이번에는 이 정보를 이용해서 실제로 스크립트 코드를 적어보자.

スクリプトの作成 (스크립트의 작성)

テストする環境はUbuntuの中でPython 3.5を使う。
すでにpiprequestslxmlライブラリーをインストールした状態である。
だが、もともとWindows 10を使っているのでChromeディベロッパーツールを使うのはWindows`からした。
要は、どこでもいいってことだ。

테스트 환경은 Ubuntu 에서 Python 3.5를 사용한다.
이미 pip로 requests와 lxml 라이브러리를 설치해둔 상태이다.
그러나 원래 Windows 10을 사용하고 있기 때문에 Chrome의 Developer Tool은 Windows에서 사용했다.
말하자면, 어디든 상관 없다는 이야기다.

テストコード #1 (테스트 코드 #1)

先ずは、スクリーンショットに出ているままで具現してみよう。

우선, 스크린 샷에 나와있는 대로 구현해보자.

ideone-test-1.py
from requests import get, post

from pprint import pprint

url = "http://ideone.com/ideone/Index/submit/"

headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" }

data = {
    "p1": "69fc2d31721791fd3912a444035b0ba6",
    "p2": 9,
    "p3": 20,
    "p4": 1710,
    "clone_link": "/",
    "file": "Lorem ipsum dolor sit amet",
    "input": '',
    "syntax": 1,
    "timelimit": 0,
    "note": '',
    "_lang": 116,
    "public": 1,
    "run": 1,
    "Submit": '',
}

rs = post(url, headers=headers, data=data)
print(rs)
pprint(dict(rs.headers))

コードの内容はLorem ipsum dolor sit ametに変更した。
もしかするとサーバーからBANされるかも知らないからヘッダーのUser-AgentChromeのものに変えた。
これで実行。

소스 코드의 내용은 'Lorem ipsum dolor sit amet' 으로 변경했다.
혹시 서버에서 밴당할까 싶어 User-Agent 헤더도 Chrome의 것으로 바꿨다.
이대로 실행.

<Response [200]>
{'Cache-Control': 'no-store, no-cache, must-revalidate, post-check=0, '
                  'pre-check=0',
 'Connection': 'Keep-Alive',
 'Content-Encoding': 'gzip',
 'Content-Length': '11067',
 'Content-Type': 'text/html',
 'Date': 'Tue, 02 Aug 2016 05:23:03 GMT',
 'Expires': 'Thu, 19 Nov 1981 08:52:00 GMT',
 'Keep-Alive': 'timeout=1, max=90',
 'Pragma': 'no-cache',
 'Server': 'Apache/2.2.22 (Debian)',
 'Set-Cookie': 'JIDEONE=d6e51e6c3ec5a653d7e1be24b87cf050; expires=Thu, '
               '02-Aug-2018 05:23:04 GMT; path=/',
 'Vary': 'Accept-Encoding',
 'X-Powered-By': 'PHP/5.4.15-1'}

200番が来る。
あ、リダイレクトするのを見逃した。post関数の引数にallow_redirects=Trueを追加して再実行する。

200번이 온다.
아, 리다이렉트한다는 것을 깜빡했다. post 함수의 인자에 allow_redirects=True 를 추가해서 재실행한다.

<Response [302]>
{'Cache-Control': 'no-store, no-cache, must-revalidate, post-check=0, '
                  'pre-check=0',
 'Connection': 'Keep-Alive',
 'Content-Encoding': 'gzip',
 'Content-Length': '20',
 'Content-Type': 'text/html',
 'Date': 'Tue, 02 Aug 2016 05:54:25 GMT',
 'Expires': 'Thu, 19 Nov 1981 08:52:00 GMT',
 'Keep-Alive': 'timeout=1, max=85',
 'Location': '/',
 'Pragma': 'no-cache',
 'Server': 'Apache/2.2.22 (Debian)',
 'Set-Cookie': 'PHPSESSID=2gg67ij2vmh97d3aapqia8sgt4; path=/',
 'Vary': 'Accept-Encoding',
 'X-Powered-By': 'PHP/5.4.15-1'}

ついに302になっているが、Locationヘッダーが間違っている。
まあ、予想していないわけでもない。おそらくCookieとかp1のハッシュの問題だろう。
今回はウェブブラウザの行動をそのまま真似してみよう。

드디어 302가 되었지만, Location 헤더가 맞지 않다.
뭐, 예상 못한 것은 아니다. 아마도 Cookie나 p1 의 해쉬코드가 문제일 것이다.
이번에는 웹 브라우저의 동작을 그대로 따라해보자.

テストコード #2 (테스트 코드 #2)

変数の根の復元 (변수 값의 복원)

コードを書く前に、いくつ確認することがある。syntaxとかpublicとかrunみたいに0でなければ1になっている変数たちはBoolean変数で推測可能が、p1, p2, p3, p4の意味が分からない。
さっそくview-source:http://ideone.com/に接続して調べましょう。

소스 코드를 적기 전에 몇 가지 확인해야 할 것이 있다. syntax나 public, run 같이 0 혹은 1로 되어있는 변수들은 Boolean 변수로 추측이 가능하지만, p1, p2, p3, p4 의 의미를 알 수가 없다.
당장 view-source:http://ideone.com/ 에 접속해 조사해보자.

0011.png

スクロールを少し下すとすぐに見える。inputタグたちのp1にハッシュが直接入っているし、p2p3にも数字が入力されている。
ところで…p4には何もない???
慌てる必要はない。必ずどこかで値を与えるはずだ。こういう場合は大体Javascriptでなっていることが多い。
<scriptとか.jsをキーワードにして検索してみましょう。

스크롤을 조금 내리니 보인다. input 태그 중 p1 에 해쉬코드가 그대로 들어가있고, p2와 p3에는 숫자가 입력되어있다.
그런데... p4에는 아무것도 없어???
당황할 필요 없다. 분명히 어딘가에서 값을 줄 것이다. 이런 경우에는 대체로 Javascript로 되어 있는 경우가 많다.
'<script' 나 '.js' 로 검색해보자.

0013.png

いろいろと見回ってみたが、あのideone-common.jsが一番気になって開けてみたら、

여기저기 둘러봤는데, 이 'ideone-common.js'가 가장 의심스러워 열어봤더니,

0014.png

見つけた。

찾았다.

変数の計算 (변수의 계산)

jQueryでなっているのでちょっと見やすい。p4protectionという関数でp1, p2, p3を因数として、その結果を代入することに見える。
しかし、問題があるそうだ。protection関数の定義が見えない。そしてあのドラッグしたやったeval部分にprotectionが見える。p,a,c,k,e,dの因数たちによると乱読化のようだ。

jQuery로 되어 있어 조금 보기 쉽다. p4에 protection이라는 함수로 p1, p2, p3를 인자로 넣어 그 결과를 대입하는 것으로 보인다.
그러나 문제가 있어 보인다. protection 함수의 정의가 보이지 않는다. 그리고 위의 드래그한 eval 부분에 protection이 보인다.
p,a,c,k,e,d 라는 인자를 보아하니 난독화된 것 같다.

0015.png

実際にディベロッパーツールで実行してみるとprotection関数が作られる。
すこし興味深いだが、今の目的はこれではない。protection関数を分析してみましょう。

실제로 개발자 도구에서 실행시켜보니 protection 함수가 만들어진다.
살짝 흥미가 동하지만 지금 목적은 이게 아니다. protection 함수를 분석해보자.

protection.js
function protection($a, $b, $c) {
    var $r = 0;
    $a = mul($c, 2);
    for (var $i = 0; $i < $c; $i++) {
        $r = add($r, _mul($i, $b))
    }
    return $r;
}

一行に書いているコードは見難いからOnline JavaScript Beautifierから行送りした。
難しい計算ではなかった。$aは全然要らないし。
略すると、

한 줄로 써 있는 소스 코드는 보기 힘들기 때문에 Online JavaScript Beautifier에서 개행했다.
어려운 계산은 아니었다. 심지어 $a는 쓰이지도 않는다.
요약해보면,

protection-summary.js
function protection(a, b, c) {
    var r = 0;
    for (var i = 0; i < c; ++i) r += i * b;
    return r;
}

こうなる。
計算式自体を整理すると、

이렇게 된다.
계산식 자체를 정리해보면,

protection-summary2.js
function protection(a, b, c) {
    return b * ((c - 1) * c) / 2;
}

こうにもなる。
今必要なのはこれ。材料はそろった。
Pythonスクリプトを書こう。

이렇게도 된다.
지금 필요한 것이 이것. 재료가 모였다.
Python 스크립트를 쓰자.

全体スクリプトコード (전체 스크립트 코드)

ideone-test-2.py
from requests import get, post
from lxml.etree import HTML
namespaces = dict(re="http://exslt.org/regular-expressions")

from pprint import pprint

''' #1: Cookie のゲット。 '''
# 先に index ページを受けて Cookie とかを保存。
# (먼저 index 페이지를 받아 Cookie 등을 저장)
url = "http://ideone.com/"
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" }
rs = get(url, headers=headers)
cookies = rs.cookies
root = HTML(rs.content if rs.ok else "<error/>")

data = {
    "file": "Lorem ipsum dolor sit amet",
    "input": '',
    "syntax": 1,
    "timelimit": 0,
    "note": '',
    "public": 1,
    "run": 1,
    "Submit": '',
}

''' #2: p1, p2, p3 から p4 を計算。 (p1, p2, p3을 이용해 p4 계산) '''
# HTML の内容を分析して XPath で p1, p2, p3 をゲット。
# (HTML의 내용을 분석하여 XPath를 이용해 p1, p2, p3을 가져온다.)
#for x in root.xpath('//*[@id="p1" or @id="p2" or @id="p3" or @id="p4"]'):
# 正規表現でこうするのも可能。
# (정규식으로 이렇게도 가능)
for x in root.xpath('//*[re:test(@id,"p[1-3]")]', namespaces=namespaces):
    data[x.get("id")] = x.get("value")

p2 = int(data["p2"])
p3 = int(data["p3"])
p4 = p2 * sum(range(p3))
data["p4"] = str(p4)


''' #3: _lang コードリストをゲット。 (_lang 코드 리스트를 획득) '''
# 言語コードのリストを直接獲得して使ってみましょう。
# (언어 코드 리스트를 직접 획득하여 사용해 보자.)
_langs = {}
#for x in root.xpath('//li/*[starts-with(@id,"menu-lang-")]'):
for x in root.xpath('//li/*[re:test(@id,"menu-lang-[0-9]+")]', namespaces=namespaces):
    _langs[x.text] = int(x.get("data-id"))

data["_lang"] = _langs["Python 3"]


url = "http://ideone.com/ideone/Index/submit/"
rs = post(url, headers=headers, cookies=cookies, data=data, allow_redirects=False)
print(rs)
pprint(dict(rs.headers))
  • #1: Session維持のためにCookieを保存した。
  • (Session 유지를 위해 Cookie를 저장)
  • #2: 先のprotection関数を真似してp4を計算した。
  • (아까 protection 함수를 따라하여 p4를 계산했다.0
  • #3: Python 3に当たる_langコードの116を直接入力せず、言語のコードのリストを全部受けてそこから選んでみた。
  • (Python 3에 맞는 _lang 코드 116을 직접 입력하지 않고, 언어 코드 리스트를 전부 받아와서 선택해보았다.)

実行したその結果は?

실행한 결과는?

<Response [302]>
{'Cache-Control': 'no-store, no-cache, must-revalidate, post-check=0, '
                  'pre-check=0',
 'Connection': 'Keep-Alive',
 'Content-Encoding': 'gzip',
 'Content-Length': '20',
 'Content-Type': 'text/html',
 'Date': 'Fri, 05 Aug 2016 06:40:25 GMT',
 'Expires': 'Thu, 19 Nov 1981 08:52:00 GMT',
 'Keep-Alive': 'timeout=1, max=79',
 'Location': '/UH3fgT',
 'Pragma': 'no-cache',
 'Server': 'Apache/2.2.22 (Debian)',
 'Set-Cookie': 'settings=%7B%22run_lang%22%3A%22116%22%7D; expires=Wed, '
               '01-Feb-2017 06:40:25 GMT; path=/, '
               'settings=%7B%22run_lang%22%3A%22116%22%2C%22run_public%22%3A%221%22%7D; '
               'expires=Wed, 01-Feb-2017 06:40:25 GMT; path=/, '
               'settings=%7B%22run_lang%22%3A%22116%22%2C%22run_public%22%3A%221%22%2C%22run_run%22%3A%221%22%7D; '
               'expires=Wed, 01-Feb-2017 06:40:25 GMT; path=/, '
               'settings=%7B%22run_lang%22%3A%22116%22%2C%22run_public%22%3A%221%22%2C%22run_run%22%3A%221%22%2C%22run_syntax%22%3A%221%22%7D; '
               'expires=Wed, 01-Feb-2017 06:40:25 GMT; path=/, '
               'settings=%7B%22run_lang%22%3A%22116%22%2C%22run_public%22%3A%221%22%2C%22run_run%22%3A%221%22%2C%22run_syntax%22%3A%221%22%2C%22run_timelimit%22%3A%220%22%7D; '
               'expires=Wed, 01-Feb-2017 06:40:25 GMT; path=/',
 'Vary': 'Accept-Encoding',
 'X-Powered-By': 'PHP/5.4.15-1'}

Locationヘッダーにアドレスがよく入っている。

Location 헤더에 주소가 잘 들어가있다.

0016.png

ページも正常に出る。
まあ、このままでも使えるし、ほとんど完成だといえるが、bashで使えるように整理してみよう。

페이지도 정상적으로 나온다.
뭐, 이대로도 쓸 수 있고, 거의 완성이라고도 할 수 있지만, bash에서 쓸 수 있도록 정리해보자.

整理コード (정리 소스 코드)

ideone.py
from os.path import exists, split, splitext
from sys import argv

from urllib.parse import urlunparse
from requests import get, post
from lxml.etree import HTML
namespaces = dict(re="http://exslt.org/regular-expressions")

from pprint import pprint


extensions = {
    ".c": "C",
    ".cpp": "C++14",
    ".java": "Java7",
    ".pl": "Perl",
    ".php": "PHP",
    ".py": "Python 3",
    ".ruby": "Ruby",
    ".sql": "SQL",
    ".vb": "VB.NET",
    ".go": "GO",
    ".js": "JavaScript (rhino)",
    ".lua": "Lua",
}

def get_ideone(_lang):
    url = "http://ideone.com/"
    headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" }
    rs = get(url, headers=headers)
    root = HTML(rs.content if rs.ok else "<error/>")

    data = {}
    for x in root.xpath('//*[re:test(@id,"p[1-3]")]', namespaces=namespaces):
        data[x.get("id")] = x.get("value")

    p2 = int(data["p2"])
    p3 = int(data["p3"])
    p4 = p2 * sum(range(p3))
    data["p4"] = str(p4)

    _langs = {}
    for x in root.xpath('//li/*[re:test(@id,"menu-lang-[0-9]+")]', namespaces=namespaces):
        _langs[x.text.upper()] = int(x.get("data-id"))
    data["_lang"] = _langs[_lang]

    return rs.cookies, data

def ideone(path, _lang=''):
    d, f = split(path)
    n, e = splitext(f)

    if len(_lang) <= 0:
        _lang = "Text"
        if e.lower() in extensions.keys():
            _lang = extensions[e.lower()]

    cookies, data = get_ideone(_lang.upper())

    data.update({
        "input": '',
        "syntax": 1,
        "timelimit": 0,
        "note": '',
        "public": 1,
        "run": 1,
        "Submit": '',
    })

    with open(path, 'r') as ro:
        data["file"] = ro.read()

    url = "http://ideone.com/ideone/Index/submit/"
    headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" }
    rs = post(url, headers=headers, cookies=cookies, data=data, allow_redirects=False)

    scheme = "http"
    netloc = "ideone.com"
    path = rs.headers["Location"]
    params = ''
    query = ''
    favorite = ''
    return _lang, urlunparse((scheme, netloc, path, params, query, favorite))

def usage(name):
    print("Usage: ./{} <file1> <file2> ...".format(name))

def main(argc, args):
    if argc == 1:
        d, f = split(__file__)
        usage(f)
    else:
        for i, x in enumerate(args[1:]):
            d, f = split(x)
            _lang, url = ideone(x)
            print(" - #{} ({}): {} - {}".format(i + 1, f, _lang, url))

if __name__ == "__main__":
    main(len(argv), argv)

ファイル拡張子のマッチングの方法がちょっと残念だが、コード内のextensions部分を修正して使うと自動的に拡張子を認識してアップロードできる。

파일 확장자를 매칭시키는 방법이 조금 아쉽지만, 소스 코드 안의 extensions 부분을 수정해서 사용하면 자동적으로 확장자를 인식하여 업로드 가능하다.

実行結果 (실행 결과)

root@q:~# python3 ideone.py ideone.py
 - #1 (ideone.py): Python 3 - http://ideone.com/5QJTRR

ちゃんと出ている。接続してみよう。http://ideone.com/5QJTRR

잘 나온다. 접속해보자.

0021.png

よし。私が書いたコードがそのまま入っている。

오케이. 내가 쓴 소스 소드가 그대로 들어가 있다.

結論 (결론)

ここまで、時間を効率的に浪費する方法についてQiitaに初めに投稿した。
私が書いたけどいつ使うか正直わからなくなってきた。
いつかは使用してみるだろう。

여기까지 시간을 효율적으로 낭비하는 방법에 대해 Qiita에 처음으로 기술하였다.
내가 적어놓고도 언제 쓸지 솔직히 모르겠다.
언젠간 쓰겠지.

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?