Hyun1008.log

블로그

#프로젝트 #NodeJS #블로그

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

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

주요 기능

  • 계정 생성 후 자신의 계정에 최대 100개까지 '캐비닛'을 생성할 수 있습니다.
  • 캐비닛은 private, public 으로 설정할 수 있습니다.
    • 이 구분은 메인 페이지 노출과만 관련이 있으므로, private이라고 하더라도 자신의 계정 페이지에는 노출됩니다.

캐릭터, 장소 관련

  • 연도별 캐릭터 생존현황과 장소의 변화를 조회할 수 있습니다.
  • 캐릭터의 세부사항, 연표, 다른 캐릭터와의 관계를 자유롭게 설정할 수 있습니다.
  • 지도를 불러와 그 위 좌표별로 장소를 설정할 수 있습니다.

시리즈, 자료실 관련

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

TODO

  • UI 다듬기
  • 작품 글자수 제한 없애기 (현재는 3000자)

create, update ...

  • 캐비닛 create, update, delete
  • 캐릭터 create, update, delete
  • 장소 create, update, delete
  • 테마송 create, update, delete
  • 시리즈 create, update, delete
  • 레퍼런스 create, update, delete
  • 글 create, update, delete

#프로젝트 #NodeJS #블로그

샘플 위키 깃허브 리포지토리

  • 개발 시작: 2024.09.06. – 2024.09.09.
  • 개발 기간: 4일

정적 위키?

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

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

이전 프로젝트의 취약점

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

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

Nuxt.js와 Github Actions

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

Nuxt는 Vue 기반의 프레임워크로, 일단 node를 사용하지만, Github pages로 배포할 경우에는 Actions에 의해 정적 페이지로 렌더된 사이트가 배포됩니다.

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

또 정적 페이지긴 하지만 나름대로의 동적 라우팅도 구현할 수 있습니다.

개선점 1. 인증키

스프레드시트의 보기/편집 권한을 얻기 위해서 두 가지 인증 방식을 사용합니다.

  1. 위키를 보여주는 서비스 계정의 인증
  2. 위키 편집을 위한 사용자 계정의 인증

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

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

개선점 2. 라우팅

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

https://wiki.rongo.moe/?d=대문

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

https://wiki.rongo.moe/대문/

으로 접속할 수 있습니다.

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

아쉬운 점

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

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

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

그럼에도 불구하고 무료로 위키를 제작할 수 있기 때문에, 한정된 주제를 가진 최대 3-4인 정도 되는 소규모 그룹에서 운영된다면 나름 좋은 솔루션이 될 수 있다고 생각합니다.

#개발과정 #NodeJS #블로그

Github Pages에서 Nuxt 배포하기

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

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

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

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

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

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

그런데 동적 라우팅, 예를 들어서

jyhyun1008.github.io/posts/123

같은 경우는 어떻게 할까요?

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

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

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

그래서 이 문제를 해결하고자 했는데, 공식 문서 를 찾아보는 것으로 의외로 간단하게 해결이 됐습니다.

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

//nuxt.config.js
import axios from 'axios'

export default {
  generate: {
    routes(callback) {
      axios
        .get('https://my-api/users')
        .then(res => {
          const routes = res.data.map(user => {
            return '/users/' + user.id
          })
          callback(null, routes)
        })
        .catch(callback)
    }
  }
}

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

그러니 사실은...

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

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

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

//nuxt.config.js

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

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

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

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

      const googleSheetUrl = `https://sheets.googleapis.com/v4/spreadsheets/------------/`
      const googleSheetParam = {
          method: 'GET',
          headers: {
              "content-type": "application/json",
              Authorization: "Bearer " + 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<wikiListArray.length; i++) {
          wikiList.push('/'+encodeURIComponent(wikiListArray[i].properties.title))
      }

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

#개발과정 #NodeJS #블로그

제가 쓰는 Nuxt 버전은 다음과 같습니다.

    "nuxt": "^2.15.8",
    "vue": "^2.7.10",

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

결국 끝장을 보긴 했어요.

일단 @nuxtjs/markdownit 모듈을 깔아줍니다.

npm i @nuxtjs/markdownit

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

//nuxt.config.js
  // Modules: https://go.nuxtjs.dev/config-modules
  modules: ['@nuxtjs/markdownit'],
  markdownit: {
    runtime: true // Support `$md()`
  },

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

// index.vue
<template>
    <div v-html="$md.render(wikiBody1)"></div>
</template>

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

이 부분은 어차피 replace 같은 걸로 따로 구현할 수 있는데, 따로 구현할지 아니면 그냥 둘지는 고민입니다.

#개발과정 #NodeJS #블로그

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

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

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

//index.vue
<template>
  <div>{{ wikiBody }}</div>
</template>

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

//index.vue
<script>
const jwt = require('jsonwebtoken');
const querystring = require("querystring");

const secretKey = "(private_key)"

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

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

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

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

//index.vue
const token = jwt.sign(
    { "iss": "(client_email)", "scope": "https://www.googleapis.com/auth/spreadsheets", "aud": "https://oauth2.googleapis.com/token" },
    secretKey,
    { algorithm: 'RS256', expiresIn: "1h", keyid: "(private_key_id)" }
    );

jsonwebtoken 모듈을 사용해서 사인을 해봅니다. 간단합니다.

jwt.sign(payload, privateKey, options)

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

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

예를 들어 algalgorithm, kidkeyid 입니다. expexpiresIn 이에요. iat는 자동으로 넣어줍니다.

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

//index.vue
export default {
    async asyncData () {

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

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

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

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

이유는 잘 모르겠지만 JSON.stringifyquerystring.stringify 로 바꾸었더니 잘 인증이 되었습니다.

//index.vue
        const googleSheetUrl = 'https://sheets.googleapis.com/v4/spreadsheets/(구글 시트 주소)/values/(시트 및 범위)'
        const googleSheetParam = {
            method: 'GET',
            headers: {
                "content-type": "application/json",
                Authorization: "Bearer " + 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 }
    }
}
</script>

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

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

초코숲을 만들다가 또 막히는 일이 있으면 포스트를 써 보도록 하겠습니다.

#프로젝트 #VanillaJS #블로그

이미지

링크

  • 개발 시작: 2024.08.23. ~ 2024.08.24.
  • 개발 기간: 이틀

구글 스프레드시트와 깃허브 페이지를 사용하여 제작한 위키입니다.

  • 구글스프레드시트의 '시트 하나' 가 문서 하나가 됩니다. 따라서... 만약 이걸로 수백 개의 문서를 가진 대형 위키를 만들려고 하면 좀 문제가 커집니다. 소형 위키에 사용하고자 합니다.
  • 문서를 편집하기 위해서는 그냥 원본 스프레드시트를 편집하는 방법이 있고(이 경우 기록이 남지 않으므로 추천하지 않아요), 자체 편집 기능을 이용하는 방법이 있습니다.

주의사항

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

왜 만들었나요?

  • 위키를 벌쳐에서 빼낼 수 있는 방법...
  • 그러나 팬덤 위키나 기존 위키 서비스를 쓰지 않는 방법
  • php 기반 무료호스팅도 안 쓸수있는 방법(이건 도메인 연결도 못함...)

을 찾다가 만들었습니다.

어떻게 만들었나요?

가능한 것

  • 문서 읽기
  • 구글 로그인, 로그아웃
  • 이미 존재하는 문서의 편집

해야하는 것

  • 없던 문서의 생성
  • 문서를 이전 버전으로 되돌리기
  • 자잘한 속성 (몇 번째 버전인지, 언제 마지막으로 수정되었는지)의 표시