Back to creative
creative 4.6 min read 198 lines

youtube-uploader

YouTube/Shorts 영상 업로드 자동화 — Manim 제작 영상을 Google OAuth로 YouTube에 업로드, 숏츠 조건 자동 검증

YouTube / Shorts 업로드

Google OAuth 기반으로 YouTube에 영상을 업로드합니다. 숏츠와 일반 영상 모두 지원.

⚠️ Playwright 방식 (youtube_upload_pw.py)은 백업으로 유지. 기본 업로더는 upload.py 사용.

전제 조건

| 항목 | 설명 |
|------|------|
| Python 3 | google-api-python-client, google-auth-httplib2, google-auth-oauthlib |
| YouTube 계정 | 업로드 대상 채널 |
| OAuth 토큰 | ~/.hermes/secrets/youtube_token.pickle (최초 auth.py로 생성) |

Google Cloud Console 설정 (최초 1회)

1. 프로젝트 생성 및 API 활성화


  • https://console.cloud.google.com/ → 새 프로젝트 생성
  • API 및 서비스라이브러리YouTube Data API v3사용 설정

2. OAuth 클라이언트 ID 생성


  • API 및 서비스사용자 인증 정보
  • 사용자 인증 정보 만들기OAuth 클라이언트 ID
  • 앱 유형: 데스크톱 앱
  • 이름: hermes
  • 승인된 리디렉션 URI: http://localhost (필수)
  • JSON 다운로드 → ~/.hermes/secrets/에 저장

3. OAuth 동의 화면 설정


  • OAuth 동의 화면 → 앱 정보 입력:
- 앱 이름: 에르메스봇
- 사용자 지원 이메일: sigco3111k@gmail.com
- 개발자 연락처: sigco3111k@gmail.com
  • 범위 추가: https://www.googleapis.com/auth/youtube.upload
  • 테스트 사용자: sigco3111k@gmail.com 추가
  • 앱 브랜딩:
- 홈페이지: https://sigco3111.github.io/hermes-landing/
- 개인정보처리방침: https://sigco3111.github.io/hermes-landing/privacy.html
- Google Search Console에서 홈페이지 소유권 확인 필수
  • 게시 상태: 테스트 (개인 용도)
- 테스터 계정으로 인증 시 "Google에서 확인하지 않은 앱" 경고 → 고급계속 클릭

4. 인증 실행

cd ~/.hermes/scripts/youtube_upload && python3 auth.py

브라우저가 열리면 Google 로그인 → 경고 무시 → 권한 승인. 성공 시 youtube_token.pickle 생성.

⚠️ Google Cloud Console 설정 함정

| 문제 | 해결 |
|------|------|
| "테스트 중이며 개발자가 승인한 테스터만 액세스 가능" | 테스트 사용자에 이메일 추가 확인. 경고 화면에서 "고급" → "이동" 클릭 |
| "승인 오류: invalid_scope" | YouTube Data API v3 활성화 확인 |
| "홈페이지 소유자 확인 필요" | Search Console에서 URL 접두어로 속성 추가 → HTML 태그로 확인 |
| "앱 이름이 일치하지 않음" | OAuth 동의 화면 앱 이름과 홈페이지 </code> 일치시키기 |<br>| "범위 근거/데모 동영상 누락" | 민감한 범위로 인증 제출 시 필요. 개인 용도는 테스트 모드 사용 |<br>| "개인적 용도라 인증 제출 불가" | 테스트 모드로 유지할 것. 프로덕션 전환 시 이 에러 발생함 |<br>| "테스트 모드에서도 access_denied" | 프로젝트가 올바른지 확인. 다른 프로젝트로 전환되었을 수 있음. Console 상단 프로젝트 선택 확인 |<br>| "redirect_uri mismatch" | OAuth 클라이언트 설정에 <code>http://localhost</code> 및 <code>http://localhost:8080</code> 모두 추가 |</p><p><h3>🔑 핵심 교훈</h3></p><p><ul><li><strong>개인 용도 = 무조건 테스트 모드</strong>: 프로덕션으로 전환하면 "개인적 용도"라서 검토 거부됨. 테스트 모드에서 테스터 등록 후 사용하는 것이 유일한 방법</li><li><strong>브랜딩/홈페이지 설정은 프로덕션 전환 시에만 필요</strong>: 테스트 모드에서는 앱 이름/이메일만 설정하면 됨</li><li><strong>토큰 위치</strong>: <code>/tmp/youtube_upload/token.json</code> (재부팅 시 소실 가능 → ~/.hermes/secrets/로 이동 권장)</li></ul></p><p><h2>사용법</h2></p><p><h3>최초 1회: 로그인 + 쿠키 저장</h3></p><p><pre><code>python3 ~/.hermes/scripts/youtube_upload_pw.py --login<br></code></pre></p><p>브라우저가 열리면 Google 계정으로 YouTube Studio에 로그인. 완료되면 쿠키가 자동 저장됩니다.</p><p><h3>일반 업로드</h3></p><p><pre><code>python3 ~/.hermes/scripts/youtube_upload_pw.py "/path/to/video.mp4" \<br> --title "영상 제목" \<br> --description "영상 설명" \<br> --tags "태그1 태그2" \<br> --privacy public<br></code></pre></p><p><h3>숏츠 업로드 (자동 검증)</h3></p><p><pre><code>python3 ~/.hermes/scripts/youtube_upload_pw.py "/path/to/shorts.mp4" \<br> --title "숏츠 제목" \<br> --privacy public \<br> --shorts<br></code></pre></p><p><h3>프로그래매틱 호출</h3></p><p><pre><code>from youtube_upload_pw import upload_video</p><p>url = upload_video(<br> file_path="video.mp4",<br> title="제목",<br> description="설명",<br> tags=["태그1", "태그2"],<br> privacy="public",<br> is_shorts=True,<br> headless=True,<br>)<br></code></pre></p><p><h2>숏츠 조건 자동 검증</h2></p><p>업로드 전 아래 조건을 자동 확인:</p><p>| 조건 | 검증 방법 | 값 |<br>|------|----------|-----|<br>| 비율 (9:16) | <code>ffprobe</code> width/height | 1080x1920 |<br>| 길이 (≤60s) | <code>ffprobe</code> duration | ≤60초 |<br>| 해상도 | <code>ffprobe</code> width | ≥720p 권장 |</p><p><pre><code># 자동 검증<br>ffprobe -v error -show_entries stream=width,height,duration -of json video.mp4 | python3 -c "<br>import json, sys<br>d = json.load(sys.stdin)['streams'][0]<br>w, h, dur = d['width'], d['height'], float(d.get('duration', 0))<br>issues = []<br>if w/h != 9/16: issues.append(f'비율 오류: {w}x{h} (9:16 필요)')<br>if dur > 60: issues.append(f'길이 초과: {dur:.1f}s (60s 이하 필요)')<br>if issues:<br> for i in issues: print(f'❌ {i}')<br> sys.exit(1)<br>else:<br> print(f'✅ 숏츠 조건 충족: {w}x{h}, {dur:.1f}s')<br>"<br></code></pre></p><p><h2>Manim 세로 영상 렌더링</h2></p><p><pre><code>manim -qm --disable_caching --resolution 1080,1920 script.py Scene1 Scene2 ...<br></code></pre></p><p><h2>옵션</h2></p><p>| 옵션 | 기본값 | 설명 |<br>|------|--------|------|<br>| <code>--title</code>, <code>-t</code> | 파일명 | 영상 제목 |<br>| <code>--description</code>, <code>-d</code> | <code>""</code> | 영상 설명 |<br>| <code>--tags</code> | <code>[]</code> | 태그 (공백 구분) |<br>| <code>--category</code> | <code>28</code> | 카테고리 (28=Science & Tech) |<br>| <code>--privacy</code>, <code>-p</code> | <code>public</code> | <code>public</code>, <code>unlisted</code>, <code>private</code> |<br>| <code>--shorts</code> | <code>false</code> | 숏츠 모드 (비율/길이 자동 검증) |<br>| <code>--login</code> | — | 로그인 + 쿠키 저장 (최초 1회) |<br>| <code>--headless</code> | <code>true</code> | 헤드리스 모드 |<br>| <code>--no-headless</code> | — | 브라우저 표시 (디버그용) |</p><p><strong>GitHub Trending 중복 방지 연동 (04-22):</strong> Notion DB(33f76f2e90978176a537d40ebb476ef9)에서 이미 업로드한 repo 확인 후 중복 제외. 속성: Uploaded(checkbox), UploadDate(date), VideoTitle(rich_text). <code>youtube_shorts_factory.py</code>의 <code>collect_github_trending()</code>에 통합됨.</p><p>YouTube 쿠키는 보통 몇 주~몇 달 유지됩니다. 만료 시 <code>--login</code> 다시 실행.</p><p><h2>🔧 기존 OAuth 방식 (백업)</h2></p><p><code>upload.py</code>는 Google Cloud Console + OAuth 기반입니다. Playwright 방식으로 전환 권장하지만, 백업으로 유지합니다.</p><p><h2>⚠️ 토큰 만료 시 인증 방법</h2></p><p><strong>Playwright 방식(<code>--login</code>)은 Google 자동화 감지로 로그인 불가. 사용하지 말 것.</strong></p><p>OAuth 토큰 만료 시 수동 인증 스크립트 사용:</p><p><pre><code>python3 /tmp/youtube_auth_manual.py<br></code></pre></p><p><ul><li>URL이 출력됨 → 브라우저에 복붙</li><li>Google 로그인 + 권한 승인</li><li><code>http://localhost/?code=...</code> 페이지로 리다이렉트됨 → 주소창의 <code>code=</code> 값 복사</li><li>터미널에 붙여넣고 Enter</li></ul></p><p>인증 완료 후 기존 <code>upload.py</code>로 정상 업로드 가능.</p><p><h2>제한사항</h2></p><p><ul><li><strong>삭제 불가</strong>: 현재 scope이 <code>youtube.upload</code>만 포함. 삭제는 YouTube 스튜디오에서 수동</li><li><strong>동시 업로드</strong>: 제한 없음</li><li><strong>일일 업로드 제한</strong>: Google 기본 쿼터 10,000 units/일 (업로드 1회 = 1,600 units → 하약 6개)</li><li><strong>토큰 만료</strong>: refresh token으로 자동 갱신됨. pickle 파일 삭제 시 재인증 필요</li></ul></div> <!-- Related Skills --> <section class="border-t border-dark-border/30 pt-10"> <h2 class="text-xl font-bold text-dark-text mb-6">Related Skills / 관련 스킬</h2> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <a href="/icbm2-skills-marketplace/skills/ace-step-music" class="group block"></a><div class="relative rounded-xl border border-dark-border/50 bg-dark-surface/30 hover:bg-dark-surface/60 hover:border-accent/30 transition-all duration-300 overflow-hidden"><a href="/icbm2-skills-marketplace/skills/ace-step-music" class="group block"> <!-- Top accent line on hover --> <div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-accent/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> </a><div class="p-5 p-4"><a href="/icbm2-skills-marketplace/skills/ace-step-music" class="group block"> <!-- Header: category badge + version + download + lang toggle --> </a><div class="flex items-center justify-between mb-3"><a href="/icbm2-skills-marketplace/skills/ace-step-music" class="group block"></a><div class="flex items-center gap-2"><a href="/icbm2-skills-marketplace/skills/ace-step-music" class="group block"></a><a href="/icbm2-skills-marketplace/category/creative" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-accent/10 text-accent border border-accent/20 hover:bg-accent/20 transition-colors" onclick="event.stopPropagation()"> creative </a> </div> <div class="flex items-center gap-1.5"> <button class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium bg-dark-bg/50 text-dark-muted border border-dark-border/30 hover:text-accent hover:border-accent/30 transition-colors cursor-pointer" onclick="event.preventDefault(); event.stopPropagation(); this.closest('.group').querySelector('.lang-ko')?.classList.toggle('hidden'); this.closest('.group').querySelector('.lang-en')?.classList.toggle('hidden'); const l = this.querySelector('.lang-label'); if(l) l.textContent = l.textContent === 'EN' ? 'KO' : 'EN';" title="Toggle Korean/English"> <span class="lang-label">EN</span> </button> </div> </div> <!-- Title --> <h3 class="font-semibold text-dark-text group-hover:text-accent transition-colors duration-200 mb-2 text-base"> ace-step-music </h3> <!-- Description --> <div class="lang-ko"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> Kaggle T4 GPU에서 ACE-Step 1.5 터보로 가사 없는 인스트루멘탈 음악 생성 — 30초~60초 곡, 프롬프트 기반 </p> </div> <div class="lang-en hidden"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> Kaggle T4 GPU에서 ACE-Step 1.5 터보로 가사 없는 인스트루멘탈 음악 생성 — 30초~60초 곡, 프롬프트 기반 </p> </div> <!-- Tags --> </div> </div> <a href="/icbm2-skills-marketplace/skills/architecture-diagram" class="group block"></a><div class="relative rounded-xl border border-dark-border/50 bg-dark-surface/30 hover:bg-dark-surface/60 hover:border-accent/30 transition-all duration-300 overflow-hidden"><a href="/icbm2-skills-marketplace/skills/architecture-diagram" class="group block"> <!-- Top accent line on hover --> <div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-accent/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> </a><div class="p-5 p-4"><a href="/icbm2-skills-marketplace/skills/architecture-diagram" class="group block"> <!-- Header: category badge + version + download + lang toggle --> </a><div class="flex items-center justify-between mb-3"><a href="/icbm2-skills-marketplace/skills/architecture-diagram" class="group block"></a><div class="flex items-center gap-2"><a href="/icbm2-skills-marketplace/skills/architecture-diagram" class="group block"></a><a href="/icbm2-skills-marketplace/category/creative" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-accent/10 text-accent border border-accent/20 hover:bg-accent/20 transition-colors" onclick="event.stopPropagation()"> creative </a> <span class="text-[11px] font-mono text-dark-muted/60 bg-dark-bg/50 px-2 py-0.5 rounded"> v1.0.0 </span> </div> <div class="flex items-center gap-1.5"> <button class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium bg-dark-bg/50 text-dark-muted border border-dark-border/30 hover:text-accent hover:border-accent/30 transition-colors cursor-pointer" onclick="event.preventDefault(); event.stopPropagation(); this.closest('.group').querySelector('.lang-ko')?.classList.toggle('hidden'); this.closest('.group').querySelector('.lang-en')?.classList.toggle('hidden'); const l = this.querySelector('.lang-label'); if(l) l.textContent = l.textContent === 'EN' ? 'KO' : 'EN';" title="Toggle Korean/English"> <span class="lang-label">EN</span> </button> </div> </div> <!-- Title --> <h3 class="font-semibold text-dark-text group-hover:text-accent transition-colors duration-200 mb-2 text-base"> architecture-diagram </h3> <!-- Description --> <div class="lang-ko"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> Generate dark-themed SVG diagrams of software systems and cloud infrastructure as standalone HTML files with inline SVG graphics. Semantic component colors (cyan=frontend, emerald=backend, violet=database, amber=cloud/AWS, rose=security, orange=message bus), JetBrains Mono font, grid background. Best suited for software architecture, cloud/VPC topology, microservice maps, service-mesh diagrams, database + API layer diagrams, security groups, message buses — anything that fits a tech-infra deck with a dark aesthetic. If a more specialized diagramming skill exists for the subject (scientific, educational, hand-drawn, animated, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback. Based on Cocoon AI's architecture-diagram-generator (MIT). </p> </div> <div class="lang-en hidden"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> Generate dark-themed SVG diagrams of software systems and cloud infrastructure as standalone HTML files with inline SVG graphics. Semantic component colors (cyan=frontend, emerald=backend, violet=database, amber=cloud/AWS, rose=security, orange=message bus), JetBrains Mono font, grid background. Best suited for software architecture, cloud/VPC topology, microservice maps, service-mesh diagrams, database + API layer diagrams, security groups, message buses — anything that fits a tech-infra deck with a dark aesthetic. If a more specialized diagramming skill exists for the subject (scientific, educational, hand-drawn, animated, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback. Based on Cocoon AI's architecture-diagram-generator (MIT). </p> </div> <!-- Tags --> </div> </div> <a href="/icbm2-skills-marketplace/skills/ascii-art" class="group block"></a><div class="relative rounded-xl border border-dark-border/50 bg-dark-surface/30 hover:bg-dark-surface/60 hover:border-accent/30 transition-all duration-300 overflow-hidden"><a href="/icbm2-skills-marketplace/skills/ascii-art" class="group block"> <!-- Top accent line on hover --> <div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-accent/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> </a><div class="p-5 p-4"><a href="/icbm2-skills-marketplace/skills/ascii-art" class="group block"> <!-- Header: category badge + version + download + lang toggle --> </a><div class="flex items-center justify-between mb-3"><a href="/icbm2-skills-marketplace/skills/ascii-art" class="group block"></a><div class="flex items-center gap-2"><a href="/icbm2-skills-marketplace/skills/ascii-art" class="group block"></a><a href="/icbm2-skills-marketplace/category/creative" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-accent/10 text-accent border border-accent/20 hover:bg-accent/20 transition-colors" onclick="event.stopPropagation()"> creative </a> <span class="text-[11px] font-mono text-dark-muted/60 bg-dark-bg/50 px-2 py-0.5 rounded"> v4.0.0 </span> </div> <div class="flex items-center gap-1.5"> <button class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium bg-dark-bg/50 text-dark-muted border border-dark-border/30 hover:text-accent hover:border-accent/30 transition-colors cursor-pointer" onclick="event.preventDefault(); event.stopPropagation(); this.closest('.group').querySelector('.lang-ko')?.classList.toggle('hidden'); this.closest('.group').querySelector('.lang-en')?.classList.toggle('hidden'); const l = this.querySelector('.lang-label'); if(l) l.textContent = l.textContent === 'EN' ? 'KO' : 'EN';" title="Toggle Korean/English"> <span class="lang-label">EN</span> </button> <a href="/icbm2-skills-marketplace/downloads/ascii-art.zip" class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 hover:bg-emerald-500/20 transition-colors" onclick="event.stopPropagation()" title="Download ascii-art"> <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path> </svg> <span class="inline">ZIP</span> </a> </div> </div> <!-- Title --> <h3 class="font-semibold text-dark-text group-hover:text-accent transition-colors duration-200 mb-2 text-base"> ascii-art </h3> <!-- Description --> <div class="lang-ko"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> pyfiglet(571폰트), cowsay, boxes, toilet 등으로 ASCII 아트 생성. API 키 불필요. </p> </div> <div class="lang-en hidden"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> Generate ASCII art using pyfiglet (571 fonts), cowsay, boxes, toilet, image-to-ascii, remote APIs (asciified, ascii.co.uk), and LLM fallback. No API keys required. </p> </div> <!-- Tags --> </div> </div> <a href="/icbm2-skills-marketplace/skills/ascii-video" class="group block"></a><div class="relative rounded-xl border border-dark-border/50 bg-dark-surface/30 hover:bg-dark-surface/60 hover:border-accent/30 transition-all duration-300 overflow-hidden"><a href="/icbm2-skills-marketplace/skills/ascii-video" class="group block"> <!-- Top accent line on hover --> <div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-accent/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> </a><div class="p-5 p-4"><a href="/icbm2-skills-marketplace/skills/ascii-video" class="group block"> <!-- Header: category badge + version + download + lang toggle --> </a><div class="flex items-center justify-between mb-3"><a href="/icbm2-skills-marketplace/skills/ascii-video" class="group block"></a><div class="flex items-center gap-2"><a href="/icbm2-skills-marketplace/skills/ascii-video" class="group block"></a><a href="/icbm2-skills-marketplace/category/creative" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-accent/10 text-accent border border-accent/20 hover:bg-accent/20 transition-colors" onclick="event.stopPropagation()"> creative </a> </div> <div class="flex items-center gap-1.5"> <button class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium bg-dark-bg/50 text-dark-muted border border-dark-border/30 hover:text-accent hover:border-accent/30 transition-colors cursor-pointer" onclick="event.preventDefault(); event.stopPropagation(); this.closest('.group').querySelector('.lang-ko')?.classList.toggle('hidden'); this.closest('.group').querySelector('.lang-en')?.classList.toggle('hidden'); const l = this.querySelector('.lang-label'); if(l) l.textContent = l.textContent === 'EN' ? 'KO' : 'EN';" title="Toggle Korean/English"> <span class="lang-label">EN</span> </button> <a href="/icbm2-skills-marketplace/downloads/ascii-video.zip" class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 hover:bg-emerald-500/20 transition-colors" onclick="event.stopPropagation()" title="Download ascii-video"> <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path> </svg> <span class="inline">ZIP</span> </a> </div> </div> <!-- Title --> <h3 class="font-semibold text-dark-text group-hover:text-accent transition-colors duration-200 mb-2 text-base"> ascii-video </h3> <!-- Description --> <div class="lang-ko"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> ASCII 아트 비디오 프로덕션 파이프라인 — 비디오/오디오/이미지를 컬러 ASCII 캐릭터 비디오(MP4, GIF)로 변환 </p> </div> <div class="lang-en hidden"> <p class="text-dark-muted text-sm leading-relaxed line-clamp-2"> Production pipeline for ASCII art video — any format. Converts video/audio/images/generative input into colored ASCII character video output (MP4, GIF, image sequence). Covers: video-to-ASCII conversion, audio-reactive music visualizers, generative ASCII art animations, hybrid video+audio reactive, text/lyrics overlays, real-time terminal rendering. Use when users request: ASCII video, text art video, terminal-style video, character art animation, retro text visualization, audio visualizer in ASCII, converting video to ASCII art, matrix-style effects, or any animated ASCII output. </p> </div> <!-- Tags --> </div> </div> </div> </section> </article> </main> <footer class="border-t border-dark-border/50 mt-16"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <div> <div class="flex items-center gap-2 mb-3"> <div class="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center"> <svg class="w-5 h-5 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"></path> </svg> </div> <span class="font-bold text-lg">ICBM2</span> </div> <p class="text-dark-muted text-sm leading-relaxed"> AI 에이전트를 위한 스킬 마켓플레이스.<br> Skills marketplace for AI agents. </p> </div> <div> <h4 class="font-semibold text-sm uppercase tracking-wider text-dark-muted mb-3">Resources</h4> <ul class="space-y-2 text-sm"> <li><a href="/icbm2-skills-marketplace/" class="text-dark-text/70 hover:text-accent transition-colors">All Skills</a></li> <li><a href="/icbm2-skills-marketplace/" class="text-dark-text/70 hover:text-accent transition-colors">Categories</a></li> <li><a href="https://github.com/sigco3111" class="text-dark-text/70 hover:text-accent transition-colors">GitHub</a></li> </ul> </div> <div> <h4 class="font-semibold text-sm uppercase tracking-wider text-dark-muted mb-3">About</h4> <ul class="space-y-2 text-sm"> <li><span class="text-dark-text/70">Built with Astro & Tailwind CSS</span></li> <li><span class="text-dark-text/70">Open Source</span></li> <li><span class="text-dark-text/70">ICBM2 Project</span></li> </ul> </div> </div> <div class="border-t border-dark-border/30 mt-8 pt-8 text-center"> <p class="text-dark-muted text-sm"> © 2026 ICBM2 Skills Marketplace. All rights reserved. </p> </div> </div> </footer> <script type="module">const n=document.getElementById("main-header");n&&window.addEventListener("scroll",()=>{window.scrollY>10?n.classList.add("shadow-lg","shadow-black/20"):n.classList.remove("shadow-lg","shadow-black/20")});window.toggleLang=function(){const s=document.querySelectorAll(".lang-ko"),t=document.querySelectorAll(".lang-en"),o=document.getElementById("lang-label"),e=!s[0]?.classList.contains("hidden");s.forEach(l=>l.classList.toggle("hidden",e)),t.forEach(l=>l.classList.toggle("hidden",!e)),o&&(o.textContent=e?"한국어":"English")};</script> </body> </html>