1. 이전까지의 요약.

알고리즘을 풀면 자동으로 글을 만들어주는 친구를 만들고싶어!!
우리 함께 사서 고생을 해보자!

내 고생의 흐름은 아래와 같다.

“알고리즘 풀이 감지 후, 업로드 하기” 를 기반으로

flowchart TD
    %% 1. 노드 정의 (따옴표로 감싸서 특수문자/한글 보호)
    nodeA["백준허브 풀이 업로드 감지"]
    nodeB["Git-Hub.io 레포 > 서브모듈 최신화"]
    nodeC["콘텐츠 생성(Ruby)"]
    nodeD["Jekyll build"]
    nodeE["GitHub Pages 배포"]

    %% 2. 연결 관계 (ID 기반 연결)
    nodeA --> |"repository dispatch"| nodeB
    nodeB --> nodeC
    nodeC --> nodeD
    nodeD --> nodeE

와 같이 수행한다.

저번 글에서 “왜, 어떻게”를 이야기 했으니,
“이렇게 했다” 를 이야기 할 차례가 온 것 같다.
이제는 “이렇게 했어요 ㅠㅠ” 를 주제로 이야기 할 수 있도록 하겠다.

2. 백준허브에서 문제 풀었는것들은 어떻게 가지고 올껀데?

이 문제는, 백준허브에서 “문풀 레포에 올리는 것”을 “github.io 폴더가 감지”를 하게 만들면 된다.
내 github.io 레포의 .github/workflow에 보면, trigger-main-repo.yml.example yml파일이 있다.

이 친구가 백준허브와 연동된 레포의 .github/workflow 안으로 들어가면 끝이다. (사실 끝이 아니다.)
링크에 있는 yml 긁어다 바로 쓰면, 에러가 뜨거나 내 레포에 올라가니 바로 올리는건 지양해주시길…

name: Trigger Main Repository Update

on:
  push:
    branches: [main, master]

permissions:
  contents: read

jobs:
  trigger:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger main repository workflow
        uses: peter-evans/repository-dispatch@v3
        with:
          token:  secrets.MAIN_REPO_TOKEN 
          repository: 사용자의 github io 레포지토리 명 
          event-type: submodule-updated
          client-payload: |
            {
              "submodule": "사용할 submodule 이름 ",
              "commit": "$ github.sha ",
              "ref": "$ github.ref "
            }
  • 이를 위해서 token작업과, sha ref 작업을 하고 해당 repo에 setting > security > secrets and variables을 해야한다.

위 코드는 쉽게 말하자면 “push가 발생”하면, “해당 repo로 trigger를 보내라” 의 역할을 하는 workflow이다.

이렇게 트리거를 보내면 update-submodule.yml 이 친구가, 해당 트리거를 감지해서 Fetch를 진행하게 된다.

3. 가지고 온 것을 어떻게 .md 형태로 만들껀데??

이렇게 만들었으면, 업데이트를 해야하지 않겠는가? submodule로 Fetch를 백날 해봐야 Jekyll이 읽을 _algorithm/**/index.md가 없으면 우리가 볼 수가 없다.
그래서 modules/Algorithm 아래 폴더 구조를 훑어서 md를 뱉는 친구들을 scripts/에 만들었다.
원래는 여기서 열심히 우리가 알고리즘에서 배웠던, recursion 을 사용하려고 했으나….
그런데 너무 귀찮아져서 /사이트 /티어 /문제 로 나누어서 3개를 만들었다… 돌아 가기만 하면 되니까….

각각의 ruby 스크립트는 아래와 같이 동작한다

  • 페이지 접근을 위한 Side-bar의 메뉴 생성
  • 티어별로 나누어져있으니 티어별로 분리
  • 각 티어에 있는 문제들 생성

3-1. generate_algorithm.rb/algorithm/ 허브 페이지

쉽게 말하자면, 플랫폼(사이트)별로 티어 링크 + 최근 풀이 링크를 모아서 _algorithm/index.md 하나를 찍어내는 스크립트다.
한글 경로가 깨지지 않게 safe_path / safe_url_path로 정리한 뒤, 마지막에 layout: algorithm, permalink: /algorithm/인 front matter와 카드 HTML을 합쳐서 쓴다.

BASE = "modules/Algorithm"
OUT_BASE = "_algorithm"
FileUtils.mkdir_p(OUT_BASE)

def safe_path(str)
  str.unicode_normalize(:nfkc)
     .gsub(/\p{Space}+/, "-")
     .gsub(/[^\w\-\p{Hangul}]/, "")
     .gsub(/-+/, "-").gsub(/^-|-$/, "").downcase
end

def safe_url_path(str)
  URI.encode_www_form_component(safe_path(str))
end

sites = Dir.glob("#{BASE}/*").select { |d| File.directory?(d) }

site_data = sites.map do |site_path|
  site_name = File.basename(site_path)
  tiers = Dir.glob("#{site_path}/*").select { |d| File.directory?(d) }
  tier_names = tiers.map { |t| File.basename(t) }

  recent_problems = tiers.flat_map do |tier|
    Dir.glob("#{tier}/*").select { |p| File.directory?(p) }
  end.sort_by { |p| File.mtime(p) }.reverse.first(3).map do |p|
    tier_name = File.basename(File.dirname(p))
    problem_name = File.basename(p)
    {
      "title" => problem_name,
      "url" => "/algorithm/#{safe_url_path(site_name)}/#{safe_url_path(tier_name)}/#{safe_url_path(problem_name)}/"
    }
  end

  { "site" => site_name, "site_safe" => safe_url_path(site_name),
    "tiers" => tier_names, "tiers_safe" => tier_names.map { |t| safe_url_path(t) },
    "recent" => recent_problems }
end

md = <<~MD
---
layout: algorithm
title: Algorithm Sites
permalink: /algorithm/
---

<div class="algorithm-grid">
#{site_data.map do |site|
  tiers_html = site["tiers"].zip(site["tiers_safe"]).map do |t, t_safe|
    "<li><a href='/algorithm/#{site['site_safe']}/#{t_safe}/'>#{t}</a></li>"
  end.join("\n")
  recent_html = site["recent"].map { |p| "<li><a href='#{p['url']}'>#{p['title']}</a></li>" }.join("\n")
  <<~CARD
  <div class="algo-card">
    <h2>#{site['site']}</h2>
    <h3>Tiers</h3>
    <ul>#{tiers_html}</ul>
    <h3>Recent Problems</h3>
    <ul>#{recent_html}</ul>
  </div>
  CARD
end.join("\n")}
</div>
MD

File.write("#{OUT_BASE}/index.md", md)

3-2. generate_tier.rb — 티어 단위 목록 페이지

BASE/*/*플랫폼/티어까지 내려가서, 그 안의 문제 폴더만 모은다.
문제가 많으면 한 번에 보기 힘드니 each_slice(20)으로 끊어서 섹션을 만든다. front matter에는 layout: tier, platform, tier, permalink: /algorithm/.../를 넣는다.

Dir.glob("#{BASE}/*/*").each do |tier_path|
  next unless File.directory?(tier_path)

  relative_raw = tier_path.sub("#{BASE}/", "")
  relative_parts = relative_raw.split(File::SEPARATOR)
  relative = relative_parts.map { |p| safe_path(p) }.join(File::SEPARATOR)

  out_dir = File.join(OUT_BASE, relative)
  FileUtils.mkdir_p(out_dir)

  problem_dirs = Dir.glob("#{tier_path}/*").select { |p| File.directory?(p) }
  next if problem_dirs.empty?

  groups = problem_dirs.each_slice(20).to_a
  sections = groups.map.with_index do |slice, idx|
    links = slice.map do |p|
      name = File.basename(p)
      safe_url = safe_url_path(name)
      "<li><a href=\"./#{safe_url}/\">#{name}</a></li>"
    end.join("\n")
    <<~HTML
    <h2>#{idx * 20 + 1} ~ #{idx * 20 + slice.size}</h2>
    <ul class="problem-grid collapsed">#{links}</ul>
    HTML
  end.join("\n")

  relative_url = relative_parts.map { |p| safe_url_path(p) }.join("/")
  platform_raw = relative_parts.first
  platform_url = safe_url_path(platform_raw)
  tier_raw = relative_parts[1] || File.basename(relative_raw)
  tier_url = safe_url_path(tier_raw)

  md = <<~MD
  ---
  layout: tier
  title: #{yaml_safe(File.basename(relative_raw))}
  platform: #{yaml_safe(platform_raw)}
  platform_url: #{yaml_safe(platform_url)}
  tier: #{yaml_safe(tier_raw)}
  tier_url: #{yaml_safe(tier_url)}
  permalink: #{yaml_safe("/algorithm/#{relative_url}/")}
  ---

  #{sections}
  MD

  File.write("#{out_dir}/index.md", md)
end

(yaml_safe는 front matter에 % 같은 문자가 들어가도 깨지지 않게 따옴표 처리하는 헬퍼다.)

3-3. generate_problem.rb — 문제 상세 페이지

README.md를 본문으로 쓰고, 지정 확장자 파일을 찾아 코드 펜스로 붙인다.
git log -1로 README 마지막 커밋 시각을 잡아 date:에 넣는 것도 여기서 한다.

SRC = "modules/Algorithm"
OUT_BASE = "_algorithm"

LANG_MAP = {
  ".py" => "python", ".cc" => "cpp", ".java" => "java",
  ".sql" => "sql", ".js" => "javascript"
}

def git_last_modified(file_path)
  dir = File.dirname(file_path)
  ts = `cd "#{dir}" && git log -1 --format="%ct" -- "#{File.basename(file_path)}"`.strip
  return Time.at(ts.to_i) if ts != ""
  File.mtime(file_path)
end

Dir.glob("#{SRC}/*").each do |platform_dir|
  next unless File.directory?(platform_dir)
  platform_raw = File.basename(platform_dir)
  platform = safe_path(platform_raw)

  Dir.glob("#{platform_dir}/*").each do |tier_dir|
    next unless File.directory?(tier_dir)
    tier_raw = File.basename(tier_dir)
    tier = safe_path(tier_raw)

    Dir.glob("#{tier_dir}/*").each do |problem|
      next unless File.directory?(problem)
      name_raw = File.basename(problem)
      name = safe_path(name_raw)

      out = File.join(OUT_BASE, platform, tier, name)
      FileUtils.mkdir_p(out)

      readme_path = "#{problem}/README.md"
      readme_content = File.exist?(readme_path) ? File.read(readme_path) : "_No description provided._"
      date = git_last_modified(readme_path)
      code_blocks = []

      LANG_MAP.each do |ext, lang|
        Dir.glob("#{problem}/*#{ext}").each do |file|
          content = File.read(file)
          code_blocks << <<~CODE
          ### 📄 #{File.basename(file)}

          ```#{lang}
          #{content}
          ```
          CODE
        end
      end

      platform_url = safe_url_path(platform_raw)
      tier_url = safe_url_path(tier_raw)
      name_url = safe_url_path(name_raw)

      md = <<~MD
      ---
      layout: problem
      title: #{yaml_safe(name_raw)}
      platform: #{yaml_safe(platform_raw)}
      platform_url: #{yaml_safe(platform_url)}
      tier: #{yaml_safe(tier_raw)}
      tier_url: #{yaml_safe(tier_url)}
      permalink: #{yaml_safe("/algorithm/#{platform_url}/#{tier_url}/#{name_url}/")}
      date: #{date}
      ---

      #{readme_content}

      ## 💡 Solutions

      #{code_blocks.join("\n")}
      MD

      File.write("#{out}/index.md", md)
    end
  end
end

(safe_path / safe_url_path / yaml_safe는 위 티어 스크립트와 동일 계열이다.)

사이드바용 generate_sidebar.rb나 플랫폼용 generate_algo_level.rb도 있지만, “보이는 페이지의 뼈대”는 위 세 덩어리로 거의 갖춰진다고 보면 된다.

4. 아니 이거 왜 안되는건데…

  • Fetch는 되는데,,, 이걸 기반으로 Auto Push가 되지 않는다… 그런데, 사실, 스케쥴러가 11시에 자동으로 업데이트 해주기 때문에, Fetch만 지금은 Fetch를 하면 스케쥴러로 업데이트를 수행 하는 중이다.