<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>블로그 &amp;mdash; Hyun1008.log</title>
    <link>https://blog.daydream.ink/jyhyun1008/tag:블로그</link>
    <description>재연의 개발블로그</description>
    <pubDate>Tue, 28 Apr 2026 00:53:15 +0900</pubDate>
    <item>
      <title>작품 설정 정리 사이트 &#39;CabinetKey&#39; 기능 개발 현황</title>
      <link>https://blog.daydream.ink/jyhyun1008/jagpum-seoljeong-jeongri-saiteu-cabinetkey-gineung-gaebal-hyeonhwang</link>
      <description>&lt;![CDATA[#프로젝트 #NodeJS #블로그&#xA;&#xA;CabinetKey(이하 캐비닛키)는, 자작 캐릭터와 이에 얽힌 배경 설정을 체계적으로 정리할 수 있게 돕는 도구입니다.&#xA;&#xA;따로 데이터베이스가 구현되어 있지 않으므로 어떤 서버에도 연합되지 않은 순정 Misskey에 연결해서 사용합니다.&#xA;&#xA;주요 기능&#xA;계정 생성 후 자신의 계정에 최대 100개까지 &#39;캐비닛&#39;을 생성할 수 있습니다.&#xA;캐비닛은 private, public 으로 설정할 수 있습니다.&#xA;  이 구분은 메인 페이지 노출과만 관련이 있으므로, private이라고 하더라도 자신의 계정 페이지에는 노출됩니다.&#xA;&#xA;캐릭터, 장소 관련&#xA;연도별 캐릭터 생존현황과 장소의 변화를 조회할 수 있습니다.&#xA;캐릭터의 세부사항, 연표, 다른 캐릭터와의 관계를 자유롭게 설정할 수 있습니다.&#xA;지도를 불러와 그 위 좌표별로 장소를 설정할 수 있습니다.&#xA;&#xA;시리즈, 자료실 관련&#xA;작품을 올리는 공간인 시리즈, 참고자료나 작중 주요 문헌 등을 올리는 공간인 라이브러리를 만들 수 있습니다.&#xA;  시리즈나 라이브러리 안에 회차별로 글, 그림을 업로드할 수 있습니다.&#xA;유튜브, 사운드클라우드에 업로드되어 있는 곡을 사운드트랙으로서 가져올 수 있습니다. &#xA;  해당 곡이 어떤 캐릭터와 관련된 것인지 설정할 수 있습니다.&#xA;시리즈나 라이브러리에 업로드된 회차에 사운드트랙 중 한 곡을 BGM으로서 불러올 수 있습니다.&#xA;시리즈나 라이브러리에 업로드된 회차에 관련 캐릭터를 지정할 수 있습니다.&#xA;&#xA;TODO&#xA;&#xA;UI 다듬기&#xA;작품 글자수 제한 없애기 (현재는 3000자)&#xA;&#xA;create, update ...&#xA;&#xA;캐비닛 create, update, delete&#xA;캐릭터 create, update, delete&#xA;장소 create, update, delete&#xA;테마송 create, update, delete&#xA;시리즈 create, update, delete&#xA;레퍼런스 create, update, delete&#xA;글 create, update, delete]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/jyhyun1008/tag:%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">프로젝트</span></a> <a href="/jyhyun1008/tag:NodeJS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">NodeJS</span></a> <a href="/jyhyun1008/tag:%EB%B8%94%EB%A1%9C%EA%B7%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">블로그</span></a></p>

<p><img src="https://for.stella.place/D1/c43e3e32-0460-4d23-b5a7-a0e2a3996d9b.webp" alt=""></p>

<p>CabinetKey(이하 캐비닛키)는, 자작 캐릭터와 이에 얽힌 배경 설정을 체계적으로 정리할 수 있게 돕는 도구입니다.</p>

<p>따로 데이터베이스가 구현되어 있지 않으므로 어떤 서버에도 연합되지 않은 순정 Misskey에 연결해서 사용합니다.</p>

<h1 id="주요-기능" id="주요-기능">주요 기능</h1>
<ul><li>계정 생성 후 자신의 계정에 최대 100개까지 &#39;캐비닛&#39;을 생성할 수 있습니다.</li>
<li>캐비닛은 <code>private</code>, <code>public</code> 으로 설정할 수 있습니다.
<ul><li>이 구분은 메인 페이지 노출과만 관련이 있으므로, <code>private</code>이라고 하더라도 자신의 계정 페이지에는 노출됩니다.</li></ul></li></ul>

<h2 id="캐릭터-장소-관련" id="캐릭터-장소-관련">캐릭터, 장소 관련</h2>
<ul><li><strong>연도별</strong> 캐릭터 생존현황과 장소의 변화를 조회할 수 있습니다.</li>
<li>캐릭터의 세부사항, 연표, 다른 캐릭터와의 관계를 자유롭게 설정할 수 있습니다.</li>
<li>지도를 불러와 그 위 좌표별로 장소를 설정할 수 있습니다.</li></ul>

<h2 id="시리즈-자료실-관련" id="시리즈-자료실-관련">시리즈, 자료실 관련</h2>
<ul><li>작품을 올리는 공간인 <strong>시리즈</strong>, 참고자료나 작중 주요 문헌 등을 올리는 공간인 <strong>라이브러리</strong>를 만들 수 있습니다.
<ul><li>시리즈나 라이브러리 안에 회차별로 글, 그림을 업로드할 수 있습니다.</li></ul></li>
<li>유튜브, 사운드클라우드에 업로드되어 있는 곡을 <strong>사운드트랙</strong>으로서 가져올 수 있습니다.
<ul><li>해당 곡이 어떤 캐릭터와 관련된 것인지 설정할 수 있습니다.</li></ul></li>
<li>시리즈나 라이브러리에 업로드된 회차에 사운드트랙 중 한 곡을 BGM으로서 불러올 수 있습니다.</li>
<li>시리즈나 라이브러리에 업로드된 회차에 관련 캐릭터를 지정할 수 있습니다.</li></ul>

<h1 id="todo" id="todo">TODO</h1>
<ul><li>UI 다듬기</li>
<li>작품 글자수 제한 없애기 (현재는 3000자)</li></ul>

<h2 id="create-update" id="create-update">create, update ...</h2>
<ul><li>캐비닛 create, update, delete</li>
<li>캐릭터 create, update, delete</li>
<li>장소 create, update, delete</li>
<li>테마송 create, update, delete</li>
<li>시리즈 create, update, delete</li>
<li>레퍼런스 create, update, delete</li>
<li>글 create, update, delete</li></ul>
]]></content:encoded>
      <guid>https://blog.daydream.ink/jyhyun1008/jagpum-seoljeong-jeongri-saiteu-cabinetkey-gineung-gaebal-hyeonhwang</guid>
      <pubDate>Sun, 17 Nov 2024 09:05:02 +0900</pubDate>
    </item>
    <item>
      <title>정적 위키 초코스프레드: 안전한 버전</title>
      <link>https://blog.daydream.ink/jyhyun1008/jeongjeog-wiki-enjin-cokoseupeuredeu-anjeonhan-beojeon</link>
      <description>&lt;![CDATA[#프로젝트 #NodeJS #블로그&#xA;&#xA;샘플 위키&#xA;깃허브 리포지토리&#xA;&#xA;개발 시작: 2024.09.06. - 2024.09.09.&#xA;개발 기간: 4일&#xA;&#xA;정적 위키?&#xA;&#xA;Github의 정적 페이지 호스팅 서비스(pages)를 사용하여 배포하는 위키입니다. 데이터베이스로는 구글 스프레드시트를 사용해요.&#xA;&#xA;그러니까 사실은 구글 스프레드시트 파서 에 가깝다고 해도, 할 말이 없습니다.&#xA;&#xA;이전 프로젝트의 취약점&#xA;&#xA;이전 프로젝트는 클라이언트 사이드에서 바닐라JS만 사용하여, API키가 그대로 노출되는 문제점이 있었습니다.&#xA;&#xA;또 사소한 문제였지만 라우팅 기능이 없었기 때문에 경로가 아닌 쿼리스트링 상에서 모든 것을 처리했던 문제도 있었습니다. 그래서 각 문서들의 주소가 복잡했어요.&#xA;&#xA;Nuxt.js와 Github Actions&#xA;&#xA;이번 버전의 초코스프레드 위키는 Nuxt로 작성되었습니다.&#xA;&#xA;Nuxt는 Vue 기반의 프레임워크로, 일단 node를 사용하지만, Github pages로 배포할 경우에는 Actions에 의해 정적 페이지로 렌더된 사이트가 배포됩니다.&#xA;&#xA;이 방식은 서버를 사용하지는 않지만, secret에 입력한 프라이빗 키를 가지고 Action에서 자동으로 렌더 되기 때문에 훨씬 안전하고, 페이지 로딩도 빠릅니다.&#xA;&#xA;또 정적 페이지긴 하지만 나름대로의 동적 라우팅도 구현할 수 있습니다.&#xA;&#xA;개선점 1. 인증키&#xA;&#xA;스프레드시트의 보기/편집 권한을 얻기 위해서 두 가지 인증 방식을 사용합니다.&#xA;&#xA;위키를 보여주는 서비스 계정의 인증&#xA;위키 편집을 위한 사용자 계정의 인증&#xA;&#xA;(1) 은 Google Action을 통해 서버사이드에서 처리한 것을 정적 렌더링하기 때문에 인증키가 보이지 않고, (2) 의 경우 구글 공식문서에서 제시하고 있는 클라이언트 사이드 인증을 사용하기 때문에 인증키가 필요없어요.&#xA;&#xA;스프레드시트의 권한 설정을 제한된 편집자들에게만 공개하고, 외부 공유 링크는 막아둘 수 있다는 점도 개선된 점 중 하나입니다.&#xA;&#xA;개선점 2. 라우팅&#xA;&#xA;예전 버전의 초코스프레드 위키에서 대문 문서로 접속하기 위한 주소는 다음과 같았습니다.&#xA;&#xA;https://wiki.rongo.moe/?d=대문&#xA;&#xA;하지만 지금은 기존의 다른 위키 서비스들과 동일하게,&#xA;&#xA;https://wiki.rongo.moe/대문/&#xA;&#xA;으로 접속할 수 있습니다.&#xA;&#xA;그래서 문서 제목은 / 를 포함할 수 없습니다. 샘플 위키의 경우 . 으로 /의 역할을 대신하고 있습니다.&#xA;&#xA;아쉬운 점&#xA;&#xA;물론 서버를 사용하지 않기 때문에, 실제 위키의 동작을 완전히 따라하지 못하는 면이 있습니다.&#xA;&#xA;대표적인 것이 편집 내용이 실시간으로 반영되지 않는다는 것입니다. 현재 리포지토리에서 설정된 자동 배포는 5분마다이며, 실제로는 5분에서 10분 정도의 간격으로 배포됩니다. 스프레드시트에서 편집된 내용이 위키에 반영되기까지는 최대 10분 정도가 소요됩니다.&#xA;&#xA;편집자들이 스프레드시트, 즉 데이터베이스에 직접 접속할 수 있다는 점도 한계라고 할 수 있습니다. 스프레드시트에 직접 접속해 시트를 삭제하거나, 내용을 전부 없던 일로 만들어버리거나 버전을 왜곡할 수 있습니다. 하지만 이런 것은 스프레드시트 자체의 버전 관리 기능을 통해 어느 정도 극복할 수 있습니다.&#xA;&#xA;그럼에도 불구하고 무료로 위키를 제작할 수 있기 때문에, 한정된 주제를 가진 최대 3-4인 정도 되는 소규모 그룹에서 운영된다면 나름 좋은 솔루션이 될 수 있다고 생각합니다.]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/jyhyun1008/tag:%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">프로젝트</span></a> <a href="/jyhyun1008/tag:NodeJS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">NodeJS</span></a> <a href="/jyhyun1008/tag:%EB%B8%94%EB%A1%9C%EA%B7%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">블로그</span></a></p>

<p><img src="https://for.stella.place/D1/webpublic-aa523af4-026d-47e0-a577-fa84690cfd60.png" alt=""></p>

<p><a href="https://wiki.rongo.moe/" rel="nofollow">샘플 위키</a>
<a href="https://github.com/jyhyun1008/chocospread/" rel="nofollow">깃허브 리포지토리</a></p>
<ul><li>개발 시작: 2024.09.06. – 2024.09.09.</li>
<li>개발 기간: 4일</li></ul>

<h2 id="정적-위키" id="정적-위키">정적 위키?</h2>

<p><strong>Github</strong>의 정적 페이지 호스팅 서비스(pages)를 사용하여 배포하는 위키입니다. 데이터베이스로는 <strong>구글 스프레드시트</strong>를 사용해요.</p>

<p>그러니까 사실은 <strong>구글 스프레드시트 파서</strong> 에 가깝다고 해도, 할 말이 없습니다.</p>

<h2 id="이전-프로젝트의-취약점" id="이전-프로젝트의-취약점">이전 프로젝트의 취약점</h2>

<p>이전 프로젝트는 클라이언트 사이드에서 바닐라JS만 사용하여, API키가 그대로 노출되는 문제점이 있었습니다.</p>

<p>또 사소한 문제였지만 라우팅 기능이 없었기 때문에 경로가 아닌 쿼리스트링 상에서 모든 것을 처리했던 문제도 있었습니다. 그래서 각 문서들의 주소가 복잡했어요.</p>

<h2 id="nuxt-js와-github-actions" id="nuxt-js와-github-actions">Nuxt.js와 Github Actions</h2>

<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/Nuxt_logo.svg/640px-Nuxt_logo.svg.png" alt=""></p>

<p>이번 버전의 초코스프레드 위키는 Nuxt로 작성되었습니다.</p>

<p>Nuxt는 Vue 기반의 프레임워크로, 일단 node를 사용하지만, <a href="https://nuxt.com/deploy/github-pages" rel="nofollow">Github pages로 배포</a>할 경우에는 Actions에 의해 정적 페이지로 렌더된 사이트가 배포됩니다.</p>

<p>이 방식은 서버를 사용하지는 않지만, secret에 입력한 프라이빗 키를 가지고 Action에서 자동으로 렌더 되기 때문에 훨씬 안전하고, 페이지 로딩도 빠릅니다.</p>

<p>또 정적 페이지긴 하지만 나름대로의 <a href="https://blog.daydream.ink/jyhyun1008/nuxt-2reul-github-pageseseo-dongjeog-rautingeuro-bildeuhagi" rel="nofollow">동적 라우팅</a>도 구현할 수 있습니다.</p>

<h3 id="개선점-1-인증키" id="개선점-1-인증키">개선점 1. 인증키</h3>

<p>스프레드시트의 보기/편집 권한을 얻기 위해서 두 가지 인증 방식을 사용합니다.</p>
<ol><li>위키를 보여주는 서비스 계정의 인증</li>
<li>위키 편집을 위한 사용자 계정의 인증</li></ol>

<p>(1) 은 Google Action을 통해 서버사이드에서 처리한 것을 정적 렌더링하기 때문에 인증키가 보이지 않고, (2) 의 경우 구글 공식문서에서 제시하고 있는 클라이언트 사이드 인증을 사용하기 때문에 인증키가 필요없어요.</p>

<p>스프레드시트의 권한 설정을 제한된 편집자들에게만 공개하고, 외부 공유 링크는 막아둘 수 있다는 점도 개선된 점 중 하나입니다.</p>

<h3 id="개선점-2-라우팅" id="개선점-2-라우팅">개선점 2. 라우팅</h3>

<p>예전 버전의 초코스프레드 위키에서 <code>대문</code> 문서로 접속하기 위한 주소는 다음과 같았습니다.</p>

<pre><code>https://wiki.rongo.moe/?d=대문
</code></pre>

<p>하지만 지금은 기존의 다른 위키 서비스들과 동일하게,</p>

<pre><code>https://wiki.rongo.moe/대문/
</code></pre>

<p>으로 접속할 수 있습니다.</p>

<p>그래서 문서 제목은 <code>/</code> 를 포함할 수 없습니다. 샘플 위키의 경우 <code>.</code> 으로 <code>/</code>의 역할을 대신하고 있습니다.</p>

<h3 id="아쉬운-점" id="아쉬운-점">아쉬운 점</h3>

<p>물론 서버를 사용하지 않기 때문에, 실제 위키의 동작을 완전히 따라하지 못하는 면이 있습니다.</p>

<p>대표적인 것이 편집 내용이 실시간으로 반영되지 않는다는 것입니다. 현재 리포지토리에서 설정된 자동 배포는 5분마다이며, 실제로는 5분에서 10분 정도의 간격으로 배포됩니다. 스프레드시트에서 편집된 내용이 위키에 반영되기까지는 최대 10분 정도가 소요됩니다.</p>

<p>편집자들이 스프레드시트, 즉 데이터베이스에 직접 접속할 수 있다는 점도 한계라고 할 수 있습니다. 스프레드시트에 직접 접속해 시트를 삭제하거나, 내용을 전부 없던 일로 만들어버리거나 버전을 왜곡할 수 있습니다. 하지만 이런 것은 스프레드시트 자체의 버전 관리 기능을 통해 어느 정도 극복할 수 있습니다.</p>

<p>그럼에도 불구하고 무료로 위키를 제작할 수 있기 때문에, 한정된 주제를 가진 최대 3-4인 정도 되는 소규모 그룹에서 운영된다면 나름 좋은 솔루션이 될 수 있다고 생각합니다.</p>
]]></content:encoded>
      <guid>https://blog.daydream.ink/jyhyun1008/jeongjeog-wiki-enjin-cokoseupeuredeu-anjeonhan-beojeon</guid>
      <pubDate>Mon, 09 Sep 2024 10:19:03 +0900</pubDate>
    </item>
    <item>
      <title>Github Pages로 Nuxt 2 프로젝트 배포 시 동적 라우팅 구현하기</title>
      <link>https://blog.daydream.ink/jyhyun1008/nuxt-2reul-github-pageseseo-dongjeog-rautingeuro-bildeuhagi</link>
      <description>&lt;![CDATA[#개발과정 #NodeJS #블로그&#xA;&#xA;Github Pages에서 Nuxt 배포하기&#xA;&#xA;초코스프레드는 별도의 서버 호스팅을 받을 필요 없이 무료로 위키를 올릴 수 있도록, 저예산/소규모 위키를 목표로 하고 있는 프로젝트입니다. (사실상 스프레드시트 파서입니다.)&#xA;&#xA;기존 초코스프레드는 정적 html 페이지를 그대로 Github pages로 deploy했었습니다.&#xA;&#xA;그런데 이렇게 페이지를 만들면 API 키가 전부 노출이 되어 안전하지도 않고, 쿼리스트링 형태로 문서 제목이 붙어 어딘가 너저분한 느낌이 들고 불편했습니다.&#xA;&#xA;그래서 이 두 가지를 개선하고자 Nuxt를 사용하게 된 것입니다.&#xA;&#xA;Nuxt에서는 pages/ 폴더 아래에 vue파일을 집어넣으면 그게 그대로 라우팅이 됩니다. 즉 pages/index.vue 는 / 로, pages/intro.vue 는 /intro/ 로 라우팅이 되는 형태입니다.&#xA;&#xA;그리고 이게 Github 액션을 사용한 빌드에서도 적용됩니다.&#xA;&#xA;그런데 동적 라우팅, 예를 들어서&#xA;&#xA;jyhyun1008.github.io/posts/123&#xA;&#xA;같은 경우는 어떻게 할까요?&#xA;&#xA;pages/posts 폴더 아래에 id.vue 와 같이 밑줄+파라미터 이름 을 넣으면 됩니다.&#xA;&#xA;그리고 당연하게도, 정적 페이지 빌드를 하면 이 페이지는 빠지게 됩니다(...)&#xA;&#xA;위키는 대문 페이지를 제외하고는 거의 대부분의 페이지가 동적 페이지로 되어 있는데, 만들었던 위키 페이지를 정적 페이지로 generate 하니, 대문을 제외한 모든 페이지가 날아가 있었습니다.&#xA;&#xA;그래서 이 문제를 해결하고자 했는데, 공식 문서 를 찾아보는 것으로 의외로 간단하게 해결이 됐습니다.&#xA;&#xA;아래는 공식 문서에서 제시하는 예시입니다.&#xA;&#xA;//nuxt.config.js&#xA;import axios from &#39;axios&#39;&#xA;&#xA;export default {&#xA;  generate: {&#xA;    routes(callback) {&#xA;      axios&#xA;        .get(&#39;https://my-api/users&#39;)&#xA;        .then(res =  {&#xA;          const routes = res.data.map(user =  {&#xA;            return &#39;/users/&#39; + user.id&#xA;          })&#xA;          callback(null, routes)&#xA;        })&#xA;        .catch(callback)&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;generate → routes 아래에 fetch를 통해 어떤 페이지를 라우팅할 지 배열을 불러와서 저장하는 맥락입니다.&#xA;&#xA;그러니 사실은...&#xA;&#xA;동적 라우팅처럼 보이는 것에 가까울 것 같습니다. 유저가 접속할 수 있는 모든 경우의 수의 페이지만큼 정적 파일을 생성하고, 이걸 deploy하는 것이기 때문입니다.&#xA;&#xA;저는 axios는 사용 안하고... 그냥 async + await fetch 로 했습니다. 아래 코드에서 secret이라든지 저에게 맞추어진 id같은 부분은 줄을 그었습니다.&#xA;&#xA;구글 아이디 인증을 하고, 스프레드 시트를 불러와서 시트의 제목마다 라우터를 만들어주는 구조입니다.&#xA;&#xA;//nuxt.config.js&#xA;&#xA;  generate: {&#xA;    async routes(callback) {&#xA;      var secretKey = process.env.PRIVATEKEY.replace(/\\n/gm, &#39;\n&#39;)&#xA;&#xA;      const token = jwt.sign(&#xA;          { &#34;iss&#34;: &#34;-------------&#34;, &#34;scope&#34;: &#34;https://www.googleapis.com/auth/spreadsheets&#34;, &#34;aud&#34;: &#34;https://oauth2.googleapis.com/token&#34; },&#xA;          secretKey,&#xA;          { algorithm: &#39;RS256&#39;, expiresIn: &#34;1h&#34;, keyid: &#34;------------&#34; }&#xA;      );&#xA;&#xA;      const googleAuthUrl = &#39;https://oauth2.googleapis.com/token&#39;&#xA;      const googleAuthParam = {&#xA;              method: &#39;POST&#39;,&#xA;              headers: {&#xA;                  &#39;content-type&#39;: &#34;application/x-www-form-urlencoded&#34;,&#xA;              },&#xA;              body: querystring.stringify({&#xA;                  granttype: &#34;urn:ietf:params:oauth:grant-type:jwt-bearer&#34;,&#xA;                  assertion: token&#xA;              })&#xA;          }&#xA;&#xA;      var authData = await fetch(googleAuthUrl, googleAuthParam)&#xA;      var authRes = await authData.json()&#xA;&#xA;      const googleSheetUrl = https://sheets.googleapis.com/v4/spreadsheets/------------/&#xA;      const googleSheetParam = {&#xA;          method: &#39;GET&#39;,&#xA;          headers: {&#xA;              &#34;content-type&#34;: &#34;application/json&#34;,&#xA;              Authorization: &#34;Bearer &#34; + authRes.accesstoken,&#xA;          },&#xA;      }&#xA;      var sheetData3 = await fetch(googleSheetUrl, googleSheetParam)&#xA;      var sheetRes3 = await sheetData3.json()&#xA;      var wikiListArray = sheetRes3.sheets&#xA;      var wikiList = []&#xA;      for (let i=0; i&lt;wikiListArray.length; i++) {&#xA;          wikiList.push(&#39;/&#39;+encodeURIComponent(wikiListArray[i].properties.title))&#xA;      }&#xA;&#xA;      const routes = wikiList&#xA;      callback(null, routes)&#xA;    }&#xA;  }&#xA;&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/jyhyun1008/tag:%EA%B0%9C%EB%B0%9C%EA%B3%BC%EC%A0%95" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">개발과정</span></a> <a href="/jyhyun1008/tag:NodeJS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">NodeJS</span></a> <a href="/jyhyun1008/tag:%EB%B8%94%EB%A1%9C%EA%B7%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">블로그</span></a></p>

<h2 id="github-pages에서-nuxt-배포하기" id="github-pages에서-nuxt-배포하기">Github Pages에서 Nuxt 배포하기</h2>

<p>초코스프레드는 별도의 서버 호스팅을 받을 필요 없이 무료로 위키를 올릴 수 있도록, 저예산/소규모 위키를 목표로 하고 있는 프로젝트입니다. (사실상 스프레드시트 파서입니다.)</p>

<p>기존 초코스프레드는 정적 html 페이지를 그대로 Github pages로 deploy했었습니다.</p>

<p>그런데 이렇게 페이지를 만들면 API 키가 전부 노출이 되어 안전하지도 않고, 쿼리스트링 형태로 문서 제목이 붙어 어딘가 너저분한 느낌이 들고 불편했습니다.</p>

<p>그래서 이 두 가지를 개선하고자 Nuxt를 사용하게 된 것입니다.</p>

<p>Nuxt에서는 pages/ 폴더 아래에 vue파일을 집어넣으면 그게 그대로 라우팅이 됩니다. 즉 <code>pages/index.vue</code> 는 <code>/</code> 로, <code>pages/intro.vue</code> 는 <code>/intro/</code> 로 라우팅이 되는 형태입니다.</p>

<p>그리고 이게 Github 액션을 사용한 빌드에서도 적용됩니다.</p>

<p>그런데 동적 라우팅, 예를 들어서</p>

<pre><code>jyhyun1008.github.io/posts/123
</code></pre>

<p>같은 경우는 어떻게 할까요?</p>

<p><code>pages/posts</code> 폴더 아래에 <code>_id.vue</code> 와 같이 밑줄+파라미터 이름 을 넣으면 됩니다.</p>

<p>그리고 당연하게도, 정적 페이지 빌드를 하면 이 페이지는 빠지게 됩니다(...)</p>

<p>위키는 대문 페이지를 제외하고는 거의 대부분의 페이지가 동적 페이지로 되어 있는데, 만들었던 위키 페이지를 정적 페이지로 generate 하니, 대문을 제외한 모든 페이지가 날아가 있었습니다.</p>

<p>그래서 이 문제를 해결하고자 했는데, <a href="https://v2.nuxt.com/docs/configuration-glossary/configuration-generate/#routes" rel="nofollow">공식 문서</a> 를 찾아보는 것으로 의외로 간단하게 해결이 됐습니다.</p>

<p>아래는 공식 문서에서 제시하는 예시입니다.</p>

<pre><code>//nuxt.config.js
import axios from &#39;axios&#39;

export default {
  generate: {
    routes(callback) {
      axios
        .get(&#39;https://my-api/users&#39;)
        .then(res =&gt; {
          const routes = res.data.map(user =&gt; {
            return &#39;/users/&#39; + user.id
          })
          callback(null, routes)
        })
        .catch(callback)
    }
  }
}
</code></pre>

<p>generate → routes 아래에 fetch를 통해 어떤 페이지를 라우팅할 지 배열을 불러와서 저장하는 맥락입니다.</p>

<h2 id="그러니-사실은" id="그러니-사실은">그러니 사실은...</h2>

<p>동적 라우팅<strong>처럼</strong> 보이는 것에 가까울 것 같습니다. <strong>유저가 접속할 수 있는 모든 경우의 수의 페이지만큼 정적 파일을 생성하고, 이걸 deploy하는 것</strong>이기 때문입니다.</p>

<p>저는 axios는 사용 안하고... 그냥 async + await fetch 로 했습니다. 아래 코드에서 secret이라든지 저에게 맞추어진 id같은 부분은 줄을 그었습니다.</p>

<p>구글 아이디 인증을 하고, 스프레드 시트를 불러와서 시트의 제목마다 라우터를 만들어주는 구조입니다.</p>

<pre><code>//nuxt.config.js

  generate: {
    async routes(callback) {
      var secretKey = process.env.PRIVATE_KEY.replace(/\\n/gm, &#39;\n&#39;)

      const token = jwt.sign(
          { &#34;iss&#34;: &#34;-------------&#34;, &#34;scope&#34;: &#34;https://www.googleapis.com/auth/spreadsheets&#34;, &#34;aud&#34;: &#34;https://oauth2.googleapis.com/token&#34; },
          secretKey,
          { algorithm: &#39;RS256&#39;, expiresIn: &#34;1h&#34;, keyid: &#34;------------&#34; }
      );

      const googleAuthUrl = &#39;https://oauth2.googleapis.com/token&#39;
      const googleAuthParam = {
              method: &#39;POST&#39;,
              headers: {
                  &#39;content-type&#39;: &#34;application/x-www-form-urlencoded&#34;,
              },
              body: querystring.stringify({
                  grant_type: &#34;urn:ietf:params:oauth:grant-type:jwt-bearer&#34;,
                  assertion: token
              })
          }

      var authData = await fetch(googleAuthUrl, googleAuthParam)
      var authRes = await authData.json()

      const googleSheetUrl = `https://sheets.googleapis.com/v4/spreadsheets/------------/`
      const googleSheetParam = {
          method: &#39;GET&#39;,
          headers: {
              &#34;content-type&#34;: &#34;application/json&#34;,
              Authorization: &#34;Bearer &#34; + authRes.access_token,
          },
      }
      var sheetData3 = await fetch(googleSheetUrl, googleSheetParam)
      var sheetRes3 = await sheetData3.json()
      var wikiListArray = sheetRes3.sheets
      var wikiList = []
      for (let i=0; i&lt;wikiListArray.length; i++) {
          wikiList.push(&#39;/&#39;+encodeURIComponent(wikiListArray[i].properties.title))
      }

      const routes = wikiList
      callback(null, routes)
    }
  }

</code></pre>
]]></content:encoded>
      <guid>https://blog.daydream.ink/jyhyun1008/nuxt-2reul-github-pageseseo-dongjeog-rautingeuro-bildeuhagi</guid>
      <pubDate>Sat, 07 Sep 2024 20:35:02 +0900</pubDate>
    </item>
    <item>
      <title>Nuxt.js 2에서 마크다운 렌더링하기</title>
      <link>https://blog.daydream.ink/jyhyun1008/nuxt-js-2eseo-makeudaun-rendeoringhagi</link>
      <description>&lt;![CDATA[#개발과정 #NodeJS #블로그&#xA;&#xA;제가 쓰는 Nuxt 버전은 다음과 같습니다.&#xA;&#xA;    &#34;nuxt&#34;: &#34;^2.15.8&#34;,&#xA;    &#34;vue&#34;: &#34;^2.7.10&#34;,&#xA;&#xA;원래 평소와 똑같이 marked 를 임포트해서 처리하려고 했는데... 계속 계속 오류가 뜨길래 Nuxt에서 마크다운을 렌더링하는 기본적인 방법이 없나 찾아보았습니다.&#xA;&#xA;결국 끝장을 보긴 했어요.&#xA;&#xA;일단 @nuxtjs/markdownit 모듈을 깔아줍니다.&#xA;npm i @nuxtjs/markdownit&#xA;&#xA;그다음 nuxt.config.js에서 모듈 부분을 다음과 같이 바꿉니다.&#xA;&#xA;//nuxt.config.js&#xA;  // Modules: https://go.nuxtjs.dev/config-modules&#xA;  modules: [&#39;@nuxtjs/markdownit&#39;],&#xA;  markdownit: {&#xA;    runtime: true // Support $md()&#xA;  },&#xA;&#xA;마크다운 렌더를 원하는 부분에 $md.render() 를 붙이면 끝.&#xA;&#xA;// index.vue&#xA;template&#xA;    div v-html=&#34;$md.render(wikiBody1)&#34;/div&#xA;/template&#xA;&#xA;다만 marked.js보다는 별로라고 생각합니다. 아주 기본적인 마크다운 렌더는 지원하지만, 체크리스트 같은 게 제대로 렌더되지 않았다는 점을 고려해 보면, sub 등의 구문도, 접었다 펼치는 것도 지원하지 않을 거라고 생각해요.&#xA;&#xA;이 부분은 어차피 replace 같은 걸로 따로 구현할 수 있는데, 따로 구현할지 아니면 그냥 둘지는 고민입니다.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/jyhyun1008/tag:%EA%B0%9C%EB%B0%9C%EA%B3%BC%EC%A0%95" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">개발과정</span></a> <a href="/jyhyun1008/tag:NodeJS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">NodeJS</span></a> <a href="/jyhyun1008/tag:%EB%B8%94%EB%A1%9C%EA%B7%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">블로그</span></a></p>

<p>제가 쓰는 Nuxt 버전은 다음과 같습니다.</p>

<pre><code>    &#34;nuxt&#34;: &#34;^2.15.8&#34;,
    &#34;vue&#34;: &#34;^2.7.10&#34;,
</code></pre>

<p>원래 평소와 똑같이 <code>marked</code> 를 임포트해서 처리하려고 했는데... 계속 계속 오류가 뜨길래 Nuxt에서 마크다운을 렌더링하는 기본적인 방법이 없나 찾아보았습니다.</p>

<p>결국 끝장을 보긴 했어요.</p>

<p>일단 <code>@nuxtjs/markdownit</code> 모듈을 깔아줍니다.</p>

<pre><code>npm i @nuxtjs/markdownit
</code></pre>

<p>그다음 <code>nuxt.config.js</code>에서 모듈 부분을 다음과 같이 바꿉니다.</p>

<pre><code>//nuxt.config.js
  // Modules: https://go.nuxtjs.dev/config-modules
  modules: [&#39;@nuxtjs/markdownit&#39;],
  markdownit: {
    runtime: true // Support `$md()`
  },
</code></pre>

<p>마크다운 렌더를 원하는 부분에 <code>$md.render()</code> 를 붙이면 끝.</p>

<pre><code>// index.vue
&lt;template&gt;
    &lt;div v-html=&#34;$md.render(wikiBody1)&#34;&gt;&lt;/div&gt;
&lt;/template&gt;
</code></pre>

<p><img src="https://for.stella.place/D1/53c30145-b8a1-4926-98db-b3917795bb49.png" alt=""></p>

<p>다만 marked.js보다는 별로라고 생각합니다. 아주 기본적인 마크다운 렌더는 지원하지만, 체크리스트 같은 게 제대로 렌더되지 않았다는 점을 고려해 보면, <code>&lt;sub&gt;</code> 등의 구문도, 접었다 펼치는 것도 지원하지 않을 거라고 생각해요.</p>

<p>이 부분은 어차피 replace 같은 걸로 따로 구현할 수 있는데, 따로 구현할지 아니면 그냥 둘지는 고민입니다.</p>
]]></content:encoded>
      <guid>https://blog.daydream.ink/jyhyun1008/nuxt-js-2eseo-makeudaun-rendeoringhagi</guid>
      <pubDate>Sat, 07 Sep 2024 12:02:44 +0900</pubDate>
    </item>
    <item>
      <title>[Nuxt.js/Vue.js] JWT를 이용해 구글 서비스 계정 인증하기</title>
      <link>https://blog.daydream.ink/jyhyun1008/nuxt-js-vue-js-jwtreul-iyonghae-gugeul-seobiseu-gyejeong-injeunghagi</link>
      <description>&lt;![CDATA[#개발과정 #NodeJS #블로그&#xA;&#xA;초코스프레드 위키를 Nuxt.js로 옮기게 되었습니다. &#xA;&#xA;왜 Next.js가 아니라 Nuxt.js 였냐 하면....별 이유는 없고 그냥 깃허브 액션에 Next가 있다는 것을 늦게 보았습니다 (...)&#xA;&#xA;하여튼, Nuxt.js는 뷰 기반이니만큼 뷰 기준으로 설명하겠습니다.&#xA;&#xA;//index.vue&#xA;template&#xA;  div{{ wikiBody }}/div&#xA;/template&#xA;&#xA;템플릿에는 가져온 스프레드시트의 컨텐츠를 담을 수 있게 준비했습니다.&#xA;&#xA;//index.vue&#xA;script&#xA;const jwt = require(&#39;jsonwebtoken&#39;);&#xA;const querystring = require(&#34;querystring&#34;);&#xA;&#xA;const secretKey = &#34;(privatekey)&#34;&#xA;&#xA;index.vue가 아니어도 상관없지만, nuxt 기준으로 pages 아래에 있는 뷰 파일에다가 작업을 해 주어야 합니다.&#xA;&#xA;jsonwebtoken 이라는 모듈을 설치하고 불러옵시다. 참고로 이 jsonwebtoken의 경우, 9.0.0이 아니라 8.5.1 버전을 설치해야 합니다. 그렇지 않으면 오류가 납니다.&#xA;querystring 모듈은 나중에 POST 요청을 보내기 위해 필요합니다.&#xA;&#xA;secretKey 변수에 받아온 json 파일의 private key를 집어넣습니다. 일단 로컬에서 실행하기 때문에 그대로 넣어주었습니다. 이후 env 파일로 옮길 예정입니다. 이대로 커밋만 안 하면 됩니다(...)&#xA;&#xA;참고로 이 json 파일은 구글 API에서 새 서비스 계정을 생성해서 받아왔습니다.&#xA;&#xA;//index.vue&#xA;const token = jwt.sign(&#xA;    { &#34;iss&#34;: &#34;�(clientemail)&#34;, &#34;scope&#34;: &#34;https://www.googleapis.com/auth/spreadsheets&#34;, &#34;aud&#34;: &#34;https://oauth2.googleapis.com/token&#34; },&#xA;    secretKey,&#xA;    { algorithm: &#39;RS256&#39;, expiresIn: &#34;1h&#34;, keyid: &#34;(privatekeyid)&#34; }&#xA;    );&#xA;&#xA;jsonwebtoken 모듈을 사용해서 사인을 해봅니다. 간단합니다.&#xA;&#xA;jwt.sign(payload, privateKey, options)&#xA;&#xA;Google에서 요구하는 JWT는 헤더에 alg (RS256), kid가 포함되어야 하고, 페이로드에 iss, scope, aud, iat, exp가 포함되어 있어야 합니다.&#xA;&#xA;이걸 일일이 변수로 만들어줘도 되지만, jwt 모듈에서는 그냥 option에 필요한 값만 넣어주면 알아서 사인을 해줍니다.&#xA;&#xA;예를 들어 alg는 algorithm, kid는 keyid 입니다. exp는 expiresIn 이에요. iat는 자동으로 넣어줍니다.&#xA;&#xA;만들어진 JWT는 https://jwt.io/ 에서 검증할 수 있습니다만, 구글 JWT의 경우 아마 유효하지 않다고 나올 겁니다. 하지만 이게 맞습니다. 구글에서 예시로 던져준 JWT도 유효하지 않다고 나옵니다.&#xA;&#xA;//index.vue&#xA;export default {&#xA;    async asyncData () {&#xA;&#xA;        const googleAuthUrl = &#39;https://oauth2.googleapis.com/token&#39;&#xA;        const googleAuthParam = {&#xA;                                    method: &#39;POST&#39;,&#xA;                                    headers: {&#xA;                                        &#39;content-type&#39;: &#34;application/x-www-form-urlencoded&#34;,&#xA;                                    },&#xA;                                    body: querystring.stringify({&#xA;                                        granttype: &#34;urn:ietf:params:oauth:grant-type:jwt-bearer&#34;,&#xA;                                        assertion: token&#xA;                                    })&#xA;                                }&#xA;&#xA;        var authData = await fetch(googleAuthUrl, googleAuthParam)&#xA;        var authRes = await authData.json()&#xA;&#xA;그 다음에 fetch를 하는 단계 입니다. nuxt이기 때문에 asyncData() 훅을 사용해서 데이터를 받아온 다음 템플릿에 붙일 수 있게 해 보았습니다.&#xA;&#xA;fetch는 다른 사이트로의 fetch들과 똑같이 진행하면 되는데요(그야, 그냥 평범한 POST니까요...), body 에서 granttype 이 자주 오류가 나더라구요.&#xA;&#xA;이유는 잘 모르겠지만 JSON.stringify를 querystring.stringify 로 바꾸었더니 잘 인증이 되었습니다.&#xA;&#xA;//index.vue&#xA;        const googleSheetUrl = &#39;https://sheets.googleapis.com/v4/spreadsheets/(구글 시트 주소)/values/(시트 및 범위)&#39;&#xA;        const googleSheetParam = {&#xA;            method: &#39;GET&#39;,&#xA;            headers: {&#xA;                &#34;content-type&#34;: &#34;application/json&#34;,&#xA;                Authorization: &#34;Bearer &#34; + authRes.access_token,&#xA;            },&#xA;        }&#xA;        var sheetData = await fetch(googleSheetUrl, googleSheetParam)&#xA;        var sheetRes = await sheetData.json()&#xA;        var wikiBody = sheetRes.valuessheetRes.values.length - 1&#xA;        return { wikiBody }&#xA;    }&#xA;}&#xA;/script&#xA;&#xA;내친김에 받은 액세스 토큰으로 구글 시트까지 가져와 보았습니다. &#xA;&#xA;가장 어려운 인증(...) 부분을 뚫었으니 이제는 다른 사이트와 똑같이 제대로 된 엔드포인트에다가 값만 잘 넘겨 주면 됩니다. 특히 시트 값 가져오는 건 GET이니까요.&#xA;&#xA;초코숲을 만들다가 또 막히는 일이 있으면 포스트를 써 보도록 하겠습니다.]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/jyhyun1008/tag:%EA%B0%9C%EB%B0%9C%EA%B3%BC%EC%A0%95" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">개발과정</span></a> <a href="/jyhyun1008/tag:NodeJS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">NodeJS</span></a> <a href="/jyhyun1008/tag:%EB%B8%94%EB%A1%9C%EA%B7%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">블로그</span></a></p>

<p>초코스프레드 위키를 Nuxt.js로 옮기게 되었습니다.</p>

<p>왜 Next.js가 아니라 Nuxt.js 였냐 하면....별 이유는 없고 그냥 깃허브 액션에 Next가 있다는 것을 늦게 보았습니다 (...)</p>

<p>하여튼, Nuxt.js는 뷰 기반이니만큼 뷰 기준으로 설명하겠습니다.</p>

<pre><code>//index.vue
&lt;template&gt;
  &lt;div&gt;{{ wikiBody }}&lt;/div&gt;
&lt;/template&gt;
</code></pre>

<p>템플릿에는 가져온 스프레드시트의 컨텐츠를 담을 수 있게 준비했습니다.</p>

<pre><code>//index.vue
&lt;script&gt;
const jwt = require(&#39;jsonwebtoken&#39;);
const querystring = require(&#34;querystring&#34;);

const secretKey = &#34;(private_key)&#34;
</code></pre>

<p><code>index.vue</code>가 아니어도 상관없지만, nuxt 기준으로 <code>pages</code> 아래에 있는 뷰 파일에다가 작업을 해 주어야 합니다.</p>

<p><code>jsonwebtoken</code> 이라는 모듈을 설치하고 불러옵시다. 참고로 이 <code>jsonwebtoken</code>의 경우, <strong>9.0.0이 아니라 8.5.1 버전을 설치해야 합니다.</strong> 그렇지 않으면 오류가 납니다.
<code>querystring</code> 모듈은 나중에 POST 요청을 보내기 위해 필요합니다.</p>

<p><code>secretKey</code> 변수에 받아온 json 파일의 private key를 집어넣습니다. 일단 로컬에서 실행하기 때문에 그대로 넣어주었습니다. 이후 env 파일로 옮길 예정입니다. 이대로 커밋만 안 하면 됩니다(...)</p>

<p>참고로 이 json 파일은 구글 API에서 새 서비스 계정을 생성해서 받아왔습니다.</p>

<pre><code>//index.vue
const token = jwt.sign(
    { &#34;iss&#34;: &#34;(client_email)&#34;, &#34;scope&#34;: &#34;https://www.googleapis.com/auth/spreadsheets&#34;, &#34;aud&#34;: &#34;https://oauth2.googleapis.com/token&#34; },
    secretKey,
    { algorithm: &#39;RS256&#39;, expiresIn: &#34;1h&#34;, keyid: &#34;(private_key_id)&#34; }
    );
</code></pre>

<p><code>jsonwebtoken</code> 모듈을 사용해서 사인을 해봅니다. 간단합니다.</p>

<pre><code>jwt.sign(payload, privateKey, options)
</code></pre>

<p>Google에서 요구하는 JWT는 헤더에 <code>alg</code> (RS256), <code>kid</code>가 포함되어야 하고, 페이로드에 <code>iss</code>, <code>scope</code>, <code>aud</code>, <code>iat</code>, <code>exp</code>가 포함되어 있어야 합니다.</p>

<p>이걸 일일이 변수로 만들어줘도 되지만, jwt 모듈에서는 그냥 option에 필요한 값만 넣어주면 알아서 사인을 해줍니다.</p>

<p>예를 들어 <code>alg</code>는 <code>algorithm</code>, <code>kid</code>는 <code>keyid</code> 입니다. <code>exp</code>는 <code>expiresIn</code> 이에요. <code>iat</code>는 자동으로 넣어줍니다.</p>

<p><img src="https://for.stella.place/D1/a2c36e3f-bfa9-4691-ba0a-69660e4d038d.png" alt=""></p>

<p>만들어진 JWT는 <a href="https://jwt.io/" rel="nofollow">https://jwt.io/</a> 에서 검증할 수 있습니다만, 구글 JWT의 경우 아마 유효하지 않다고 나올 겁니다. 하지만 이게 맞습니다. 구글에서 예시로 던져준 JWT도 유효하지 않다고 나옵니다.</p>

<p><img src="https://for.stella.place/D1/bef69651-2aca-45c0-89fd-57cb61017456.png" alt=""></p>

<pre><code>//index.vue
export default {
    async asyncData () {

        const googleAuthUrl = &#39;https://oauth2.googleapis.com/token&#39;
        const googleAuthParam = {
                                    method: &#39;POST&#39;,
                                    headers: {
                                        &#39;content-type&#39;: &#34;application/x-www-form-urlencoded&#34;,
                                    },
                                    body: querystring.stringify({
                                        grant_type: &#34;urn:ietf:params:oauth:grant-type:jwt-bearer&#34;,
                                        assertion: token
                                    })
                                }

        var authData = await fetch(googleAuthUrl, googleAuthParam)
        var authRes = await authData.json()
</code></pre>

<p>그 다음에 <code>fetch</code>를 하는 단계 입니다. nuxt이기 때문에 <code>asyncData()</code> 훅을 사용해서 데이터를 받아온 다음 템플릿에 붙일 수 있게 해 보았습니다.</p>

<p><code>fetch</code>는 다른 사이트로의 <code>fetch</code>들과 똑같이 진행하면 되는데요(그야, 그냥 평범한 POST니까요...), <code>body</code> 에서 <code>grant_type</code> 이 자주 오류가 나더라구요.</p>

<p>이유는 잘 모르겠지만 <code>JSON.stringify</code>를 <code>querystring.stringify</code> 로 바꾸었더니 잘 인증이 되었습니다.</p>

<pre><code>//index.vue
        const googleSheetUrl = &#39;https://sheets.googleapis.com/v4/spreadsheets/(구글 시트 주소)/values/(시트 및 범위)&#39;
        const googleSheetParam = {
            method: &#39;GET&#39;,
            headers: {
                &#34;content-type&#34;: &#34;application/json&#34;,
                Authorization: &#34;Bearer &#34; + authRes.access_token,
            },
        }
        var sheetData = await fetch(googleSheetUrl, googleSheetParam)
        var sheetRes = await sheetData.json()
        var wikiBody = sheetRes.values[sheetRes.values.length - 1][2]
        return { wikiBody }
    }
}
&lt;/script&gt;
</code></pre>

<p>내친김에 받은 액세스 토큰으로 구글 시트까지 가져와 보았습니다.</p>

<p>가장 어려운 인증(...) 부분을 뚫었으니 이제는 다른 사이트와 똑같이 제대로 된 엔드포인트에다가 값만 잘 넘겨 주면 됩니다. 특히 시트 값 가져오는 건 GET이니까요.</p>

<p>초코숲을 만들다가 또 막히는 일이 있으면 포스트를 써 보도록 하겠습니다.</p>
]]></content:encoded>
      <guid>https://blog.daydream.ink/jyhyun1008/nuxt-js-vue-js-jwtreul-iyonghae-gugeul-seobiseu-gyejeong-injeunghagi</guid>
      <pubDate>Sat, 07 Sep 2024 08:58:47 +0900</pubDate>
    </item>
    <item>
      <title>Google API를 활용한 정적 위키엔진, 초코스프레드</title>
      <link>https://blog.daydream.ink/jyhyun1008/cokoseupeuredeu-wiki</link>
      <description>&lt;![CDATA[#프로젝트 #VanillaJS #블로그&#xA;&#xA;이미지&#xA;&#xA;링크&#xA;&#xA;개발 시작: 2024.08.23. ~ 2024.08.24.&#xA;개발 기간: 이틀&#xA;&#xA;구글 스프레드시트와 깃허브 페이지를 사용하여 제작한 위키입니다.&#xA;&#xA;구글스프레드시트의 &#39;시트 하나&#39; 가 문서 하나가 됩니다. 따라서... 만약 이걸로 수백 개의 문서를 가진 대형 위키를 만들려고 하면 좀 문제가 커집니다. 소형 위키에 사용하고자 합니다.&#xA;문서를 편집하기 위해서는 그냥 원본 스프레드시트를 편집하는 방법이 있고(이 경우 기록이 남지 않으므로 추천하지 않아요), 자체 편집 기능을 이용하는 방법이 있습니다.&#xA;&#xA;주의사항&#xA;&#xA;API 제한 설정: 구글에서  API키를 받을 때 범위를 특정 웹사이트, 스프레드시트로만 설정합니다.&#xA;연결할 스프레드시트의 제목은 어떤 것이어도 괜찮습니다. ID만 제대로 따면 됩니다.&#xA;링크가 있는 모든 사람이 볼 수 있음 으로 하고, 링크를 복사한 뒤 d/와 /edit 사이에 있는 문자열 복사&#xA;&#xA;왜 만들었나요?&#xA;&#xA;위키를 벌쳐에서 빼낼 수 있는 방법... &#xA;그러나 팬덤 위키나 기존 위키 서비스를 쓰지 않는 방법&#xA;php 기반 무료호스팅도 안 쓸수있는 방법(이건 도메인 연결도 못함...) &#xA;&#xA;을 찾다가 만들었습니다.&#xA;&#xA;어떻게 만들었나요?&#xA;&#xA;구글 스프레드시트 API 문서 중에 자바스크립트 빠른 시작 이라는 게 있습니다. 이걸 기본으로 해서 제작했습니다.&#xA;불러오고 편집하는 것은 셀 값 읽기 및 쓰기에 대략적인 내용이 있습니다.&#xA;마크다운 파싱은 Marked.js를 사용했습니다.&#xA;&#xA;가능한 것&#xA;&#xA;문서 읽기&#xA;구글 로그인, 로그아웃&#xA;이미 존재하는 문서의 편집&#xA;&#xA;해야하는 것&#xA;&#xA;없던 문서의 생성&#xA;문서를 이전 버전으로 되돌리기&#xA;자잘한 속성 (몇 번째 버전인지, 언제 마지막으로 수정되었는지)의 표시]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/jyhyun1008/tag:%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">프로젝트</span></a> <a href="/jyhyun1008/tag:VanillaJS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">VanillaJS</span></a> <a href="/jyhyun1008/tag:%EB%B8%94%EB%A1%9C%EA%B7%B8" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">블로그</span></a></p>

<p><img src="https://for.stella.place/D1/webpublic-aa523af4-026d-47e0-a577-fa84690cfd60.png" alt="이미지"></p>

<p><a href="https://wiki.rongo.moe" rel="nofollow">링크</a></p>
<ul><li>개발 시작: 2024.08.23. ~ 2024.08.24.</li>
<li>개발 기간: 이틀</li></ul>

<p>구글 스프레드시트와 깃허브 페이지를 사용하여 제작한 위키입니다.</p>
<ul><li>구글스프레드시트의 &#39;시트 하나&#39; 가 문서 하나가 됩니다. 따라서... 만약 이걸로 수백 개의 문서를 가진 대형 위키를 만들려고 하면 좀 문제가 커집니다. 소형 위키에 사용하고자 합니다.</li>
<li>문서를 편집하기 위해서는 그냥 <strong>원본 스프레드시트</strong>를 편집하는 방법이 있고(이 경우 기록이 남지 않으므로 추천하지 않아요), <strong>자체 편집 기능</strong>을 이용하는 방법이 있습니다.</li></ul>

<h2 id="주의사항">주의사항</h2>
<ul><li>API 제한 설정: 구글에서  API키를 받을 때 범위를 특정 웹사이트, 스프레드시트로만 설정합니다.</li>
<li>연결할 스프레드시트의 제목은 어떤 것이어도 괜찮습니다. ID만 제대로 따면 됩니다.</li>
<li>링크가 있는 모든 사람이 <strong>볼 수 있음</strong> 으로 하고, 링크를 복사한 뒤 d/와 /edit 사이에 있는 문자열 복사</li></ul>

<h2 id="왜-만들었나요" id="왜-만들었나요">왜 만들었나요?</h2>
<ul><li>위키를 벌쳐에서 빼낼 수 있는 방법...</li>
<li>그러나 팬덤 위키나 기존 위키 서비스를 쓰지 않는 방법</li>
<li>php 기반 무료호스팅도 안 쓸수있는 방법(이건 도메인 연결도 못함...)</li></ul>

<p>을 찾다가 만들었습니다.</p>

<h2 id="어떻게-만들었나요" id="어떻게-만들었나요">어떻게 만들었나요?</h2>
<ul><li>구글 스프레드시트 API 문서 중에 <a href="https://developers.google.com/sheets/api/quickstart/js?hl=ko" rel="nofollow">자바스크립트 빠른 시작</a> 이라는 게 있습니다. 이걸 기본으로 해서 제작했습니다.</li>
<li>불러오고 편집하는 것은 <a href="https://developers.google.com/sheets/api/guides/values?hl=ko" rel="nofollow">셀 값 읽기 및 쓰기</a>에 대략적인 내용이 있습니다.</li>
<li>마크다운 파싱은 <a href="https://marked.js.org/" rel="nofollow">Marked.js</a>를 사용했습니다.</li></ul>

<h2 id="가능한-것" id="가능한-것">가능한 것</h2>
<ul><li>문서 읽기</li>
<li>구글 로그인, 로그아웃</li>
<li>이미 존재하는 문서의 편집</li></ul>

<h2 id="해야하는-것" id="해야하는-것">해야하는 것</h2>
<ul><li>없던 문서의 생성</li>
<li>문서를 이전 버전으로 되돌리기</li>
<li>자잘한 속성 (몇 번째 버전인지, 언제 마지막으로 수정되었는지)의 표시</li></ul>
]]></content:encoded>
      <guid>https://blog.daydream.ink/jyhyun1008/cokoseupeuredeu-wiki</guid>
      <pubDate>Sat, 24 Aug 2024 00:10:34 +0900</pubDate>
    </item>
  </channel>
</rss>