Tailwind v4로 올렸는데 tailwind.config.js가 무시되고 있었다 — @theme 함정과 Chrome DevTools MCP로 검증한 과정

디자인 시스템 문서에는 Primary #3182F6 Toss Blue라고 분명히 적혀 있었다. 그런데 Submit 버튼은 어쩐지 다른 파란색이었다. 기분 탓이 아니라 조금 더 보라끼가 돌고 푸르스름했다. 처음엔 모니터 캘리브레이션 문제인가 싶었는데, DevTools로 computed style을 찍어보니 background-color: oklch(0.546 0.245 262.881). 예상한 rgb(49, 130, 246) 하고는 확실히 다른 값이었다. 원인을 찾아보니 Tailwind CSS v4에서 tailwind.config.js를 완전히 무시하고 있었다. 프로젝트는 tailwindcss@4.1.18 로 올라가 있는데 설정은 v3 문법(JavaScript object) 그대로였고, @theme 블록에는 주요 컬러 스케일이 일부만 포팅돼 있었다. 결과적으로 커스텀 토큰은 전혀 적용이 안 되고 v4 기본 OKLCH 팔레트로 렌더되던 상태. ...

2026-04-21 09:00 · 8분 소요 · Seunghan

HWP 변환기를 MCP 툴로 만들기 — 기존 Gateway에 3줄 추가하는 법

오픈소스로 만든 Rust 문서 변환기 MDM(Markdown-Media)을 AI agent에서 직접 호출할 수 있게 MCP 서버로 노출하는 작업을 했다. 처음엔 @mdm/mcp-server 독립 Node.js 패키지로 만들 생각이었는데, 이미 운영 중인 Korea Law Hub Gateway에 tool 3개 추가하는 쪽이 훨씬 낫다는 결론이 나왔다. 이 글은 그 판단 과정과 실제 구현에서 걸린 지점들을 정리한다. 상황 MDM은 HWP, HWPX, PDF, DOCX, PPTX, XLSX, HTML, CSV, TXT를 Markdown으로 바꾸는 Rust 변환기다. 데스크톱 앱, PyPI 패키지(pip install mdm-parser), CLI 바이너리는 이미 있었다. 남은 건 Claude Code, Cursor, Continue.dev 같은 AI agent에서 MCP 프로토콜로 직접 부르는 경로. ...

2026-04-15 09:00 · 5분 소요 · Seunghan

LLM이 지어낸 법령을 DB로 걸러내기 — 한국어 법률 인용 환각 방지 실전

법률 AI 서비스를 만들다 보면 이런 순간이 온다. LLM이 자신감 있게 “민법 제103조의2에 따라…” 라고 답변을 줬는데, 확인해보니 제103조의2라는 조문은 존재하지 않는다. 본조인 제103조만 있고 가지조문은 만들어진 것이다. 이게 얼마나 심각한 일인지는 이미 유명한 사건이 증명했다. 2023년 미국 Mata v. Avianca 소송에서 뉴욕의 한 변호사가 ChatGPT가 생성한 판례를 법원 제출서류에 인용했다가 제재를 받았다. 판례가 전부 지어낸 것이었던 거다. 법률 도메인에서 AI 환각은 그냥 버그가 아니라 법률 책임 문제로 번진다. 이번 작업에서 내가 만든 서비스도 같은 위험에 노출돼 있었다. 사용자가 법령 개정 diff를 보면서 “이 개정이 우리 회사에 어떤 영향?” 같은 후속 질문을 하면, LLM이 답변을 돌려주면서 근거 조문을 인용한다. 그 인용이 진짜인지 아닌지를 사용자에게 떠넘길 수는 없었다. ...

2026-04-09 09:00 · 10분 소요 · Seunghan

AI로 디자인 시스템 마이그레이션했는데 사실 CSS 리스킨이었다 — Token/Component/Template/Page 4-layer 재설계 회고

Rails 8 + Hotwire 프로젝트에 iOS 26 Liquid Glass 디자인 시스템을 전면 도입했다. 7주간 39개 페이지를 마이그레이션했고, 디자인 가이드 위반 417건이 0건이 됐다. 18개 파일로 구성된 마이그레이션 설계서도 있었고, 단계별(Phase 1-7) 체크리스트도 있었다. 스스로 만족했다. 그 다음에 사용자가 한마디 했다. “토큰/컴포넌트/템플릿/페이지 형태로 반영이되어야하는데 디자인시스템부터 점검해” 그 문장 하나로 전부 무너졌다. 점검해보니 내가 한 건 디자인 시스템이 아니라 CSS 리스킨이었다. 이 글은 그 깨달음과 재설계 과정의 기록이다. AI 코딩의 함정 — 페이지마다 “시스템처럼” 보이게 만들기 처음에는 단계별로 잘 진행했다고 생각했다. 토큰 파일을 만들었고(iOS 26 79개 컬러 × 4모드), 6개 공통 ERB 파셜을 만들었고(toolbar, list_row, button 등), 각 페이지마다 전용 CSS 파일을 분리했다. 39개 페이지가 모두 새 디자인으로 바뀌었고, grep으로 위반을 측정했더니 0건이었다. ...

2026-04-08 09:00 · 10분 소요 · Seunghan

프로필 페이지 발표자료 핀 시스템 — Instagram 스타일 +N 오버레이 카드 만들기

공개 프로필 페이지를 만들고 있었다. link-in-bio 스타일로, /@username 경로에서 사용자의 소개, 링크, 발표자료를 보여주는 페이지다. 발표자료가 9개 올라가 있었는데, 전부 나열하니까 프로필이 포트폴리오 사이트처럼 변해버렸다. 스크롤이 길어지고, 정작 중요한 링크들이 묻혔다. 사용자가 원하는 3개만 “핀"해서 보여주고, 나머지는 별도 페이지로 유도하는 게 맞았다. 그런데 “더보기"를 어떻게 보여줄지가 문제였다. 별도 버튼? 빈 카드? 결국 Instagram 앨범처럼 마지막 썸네일 위에 반투명 오버레이를 올리는 방식으로 갔다. 이 글은 그 과정의 기록이다. 기존 구조: 전부 보여주기 처음 구현은 단순했다. 컨트롤러에서 published.on_profile 스코프로 가져온 발표자료를 전부 넘기고, 프론트에서 2열 그리드로 렌더링했다. ...

2026-04-05 00:00 · 7분 소요 · Seunghan

Flutter에서 Gemini, OpenAI, Claude 직접 연동하기 — 멀티 AI 프로바이더 패턴 구현

시작 — 하드코딩된 API 키 문제 Flutter 앱에서 AI 기능(영수증 OCR, 이미지 번역, 블로그 자동 생성)을 넣을 때, 처음에는 BizRouter라는 AI 프록시 서비스를 썼다. 모든 요청을 하나의 엔드포인트로 보내면 내부에서 Gemini, GPT, Claude 등으로 라우팅해주는 구조였다. 문제는 API 키가 소스 코드에 하드코딩되어 있다는 것이었다. class BizRouterService { static const _apiKey = 'sk-br-v1-d6872ae8e164...'; // 이게 코드에 그대로 static const _baseUrl = 'https://api.bizrouter.ai/v1'; 지인들에게 배포하는 MVP라 처음에는 괜찮았지만, 사용자가 자기 키를 입력해서 쓸 수 있게 만들어야 했다. Gemini 키가 있는 사람은 Gemini로, OpenAI 키가 있는 사람은 GPT로, Claude 키가 있는 사람은 Claude로 — 각자 가진 키를 쓸 수 있어야 했다. ...

2026-03-26 00:00 · 9분 소요 · Seunghan

Claude Code Channels 완전 가이드 — Telegram으로 로컬 AI 세션 원격 조종하기

2026년 3월 20일, Anthropic이 Claude Code Channels 리서치 프리뷰를 공개했다. 한마디로 요약하면, Telegram이나 Discord에서 메시지를 보내면 집에 있는 내 Mac의 Claude Code가 코드를 짜고 파일을 수정한 뒤 결과를 답장으로 보내주는 기능이다. 폰에서 “auth.py 버그 고쳐줘” 보내면 → 맥미니 Claude가 코드 파일 열고 수정하고 → “완료했습니다, 커밋했어요” 답장이 오는 식이다. 설정하면서 꽤 삽질을 했다. 이 글은 그 과정을 그대로 기록한 문서다. Claude Code Channels가 뭔가 기본 아키텍처 Claude Code Channels는 MCP(Model Context Protocol) 기반 플러그인이다. Claude Code 세션 안에 Telegram 또는 Discord와 연결된 MCP 서버를 서브프로세스로 띄우고, 외부 메시지를 세션 안으로 밀어넣는(push) 구조다. ...

2026-03-22 00:00 · 9분 소요 · Seunghan

RAG의 한계와 에이전트 기반 하이브리드 검색 — 청킹은 요약본을 주고 풀본을 쓰라는 것과 같다

들어가며 — “RAG면 충분하지 않냐?” 회사에서 AI 챗봇 도입 논의가 있었다. 누군가가 이렇게 말했다. “사이트맵 정도 수준에서 간단한 RAG는 충분히 가능하지 않냐, 비싼 돈 들이지 않고서.” 틀린 말은 아니다. FAQ 수준의 챗봇이라면 RAG(Retrieval Augmented Generation)로 충분하다. 하지만 내가 직접 MVP를 만들어보면서 깨달은 건, 복잡한 업무 문서를 다루는 챗봇에서는 RAG만으로 부족하다는 것이었다. 수백 페이지짜리 서류를 청킹하고, 임베딩하고, 벡터 DB에 넣고, 리랭킹까지 해봤다. 결과는? AI에게 책의 요약본을 주고 “전체 내용에 대해 완전하게 답해라"고 하는 것과 같았다. ...

2026-03-22 00:00 · 8분 소요 · Seunghan

판례(Case Law)로 예외 처리 설계하기 — CanonCode에서 배운 패턴

코드에서 가장 무서운 건 catch 블록 안에 숨어있는 비즈니스 결정이다. “왜 여기서 422를 반환하지? 500이 아니라?” — 이유를 알려면 git blame → PR → 슬랙 스레드를 거슬러 올라가야 한다. 3개월 전 코드면 작성자 본인도 기억 못 한다. CanonCode의 판례(Case Law) 시스템은 이 문제를 정면으로 해결한다. 모든 예외 처리의 “왜"를 구조화된 형식으로 기록하는 것이다. 법원이 판례를 남기듯. 이 글에서는 LaunchCrew 프로젝트에서 작성한 6가지 판례와, 각각이 실제 코드에 어떻게 반영됐는지를 정리한다. 판례가 왜 필요한가 예외 처리 코드에는 3가지 정보가 필요하다: ...

2026-03-21 00:00 · 6분 소요 · Seunghan

CanonCode + LLM — 명세를 주면 코드가 정확해진다는 실험 결과

LLM에게 “에스크로 결제 구현해줘"라고 하면 무슨 일이 일어나는가? 일단 뭔가 만들어 준다. 트랜잭션도 넣고, 잔액 체크도 하고, 에러 핸들링도 한다. 그런데 “우리 프로젝트에서 에스크로가 정확히 어떤 규칙으로 동작하는지"는 모른다. 추측이 섞인다. 내 프로젝트의 에스크로는 포인트 기반인데, 카드 결제를 가정한 코드가 나온다든가. CanonCode를 만들면서 예상치 못한 이점을 발견했다. .lex 명세를 LLM의 컨텍스트로 제공하면, 추측이 사라진다. 이 글에서는 3가지 케이스에서 “명세 없이” vs “명세 있이” 코드 생성 결과를 비교한다. 실험 설계 조건 모델: Claude (Sonnet) 프로젝트: LaunchCrew (Rails 8 + Inertia.js + Svelte 5) 비교 A: 자연어 프롬프트만 제공 비교 B: .lex 명세 + 자연어 프롬프트 제공 평가: 생성된 코드가 실제 프로젝트 요구사항과 얼마나 일치하는지 3가지 케이스: ...

2026-03-20 00:00 · 6분 소요 · Seunghan

CanonCode의 Rust 엔진 — .lex 파서부터 조항 충돌 감지까지

CanonCode를 소개하는 이전 글에서 “2,800줄의 코드를 160줄의 명세로 압축했다"는 실험을 다뤘다. 이번에는 그 명세를 읽고 검증하는 엔진의 내부를 파헤친다. 왜 Rust인가, 파싱은 어떻게 하는가, 조항 간 충돌은 어떻게 감지하는가. 설계 결정마다 “다른 선택지도 있었는데 왜 이걸 골랐는지"를 함께 기록한다. 왜 Rust인가 CanonCode의 엔진은 3가지 역할을 한다: .lex 파일 파싱 및 구조 검증 조항 간 의존성 그래프 빌드 헌법-법률 계층 간 충돌 감지 처음에는 TypeScript로 작성했다. Node.js 생태계에 익숙하고, JSON 파싱이 네이티브이니까. 한 달 정도 쓰다가 Rust로 재작성했다. 이유: ...

2026-03-19 00:00 · 7분 소요 · Seunghan

코드 2,800줄을 명세 160줄로 — CanonCode로 실제 프로젝트를 변환해본 결과

코드가 커질수록 “이 기능이 왜 이렇게 동작하지?“를 알려면 파일 5개를 열어봐야 한다. 설계 문서는 3개월 전에 작성된 채로 방치되어 있고, 실제 코드와 일치하는지 아무도 모른다. 주석은 낡았고, 슬랙 스레드는 지워졌으며, 원래 기획자는 퇴사했다. 만약 설계 문서 자체가 실행 가능하고, 코드 대신 그 문서를 유지보수한다면? CanonCode라는 사이드 프로젝트에서 이 아이디어를 실험해봤다. 실제 프로덕션 수준 프로젝트에 적용한 결과와, 그 과정에서 마주친 현실적인 문제들을 기록한다. 아이디어: 법률 체계로 소프트웨어를 거버넌스한다 법률 시스템에서 영감을 받았다: ...

2026-03-18 00:00 · 6분 소요 · Seunghan
Stimulus DnD Collapse Dashboard

Rails 대시보드에 DnD 카드 순서 변경 + 접기 구현 — SortableJS + Stimulus + CSS 트릭

스포츠 대회 관리 앱의 대시보드에 두 가지 기능을 추가하는 작업이었다. 카드 순서 DnD 변경 — 내 경기 / 대진표 / 경기 목록 카드를 원하는 순서로 재배치 카드 접기/펼치기 — 관심 없는 섹션을 접어 화면을 간결하게 각각은 단순해 보이지만, Turbo Frame lazy loading과 함께 동작해야 하고, 새로고침 후에도 상태가 유지되어야 한다는 조건이 붙으면 신경 쓸 게 늘어난다. 1. DnD 라이브러리 선택 처음에는 native HTML5 Drag & Drop API로 직접 구현했다. dragstart, dragover, drop 이벤트를 다 붙이고 DOM 조작으로 순서를 바꾸는 방식인데, 실제로 동작하게 만드는 건 어렵지 않다. ...

2026-03-17 00:00 · 5분 소요 · Seunghan
AI 에이전트 개발 과정

[개발일기] Rails 8로 AI 에이전트 리뷰 시스템 만들면서 삽질한 이야기

AI가 글을 검수해주는 시스템을 Rails 8로 만들고 있다. 4개의 AI 에이전트가 각자 관점에서 원고를 분석하고, 스토리 데이터베이스와 연동해서 일관성까지 체크하는 구조. 만들면서 꽤 많이 삽질했는데, 기록 안 해두면 까먹을 것 같아서 정리해본다. 1. AI 에이전트의 “톤"이 이렇게 중요할 줄이야 처음엔 에이전트 프롬프트를 이렇게 썼다: 당신은 편집 보조자입니다. 원고를 분석하고 문제점을 지적하세요. 테스트 유저한테 피드백을 받았는데, **“이건 도움이 아니라 채점이다”**라는 반응이 돌아왔다. 창작하는 사람 입장에서 “지적” 톤은 부담스럽다는 거였다. 업계 리서치를 해보니까 Sudowrite, NovelAI 같은 도구들도 “동료” 톤이 압도적으로 선호된다고 한다. ...

2026-03-12 00:00 · 7분 소요 · Seunghan

Rails 깜짝 과제 기능 + 1회성 알림 배너 — 기존 모델 재활용과 localStorage 활용

스터디를 운영하다 보면 세션 중간에 즉석으로 과제를 내야 할 때가 있다. 기존 관리자 페이지를 통하면 여러 단계를 거쳐야 하고, 멘티들은 새 과제가 생긴 걸 바로 알 수 없다는 문제가 있었다. 이 글에서는 새 모델 없이 기존 시스템을 재활용하여 깜짝 과제 기능을 만들고, 1회성 알림 배너로 멘티에게 즉시 알려주는 구현 과정을 정리한다. 문제 정의 과제 생성이 느리다: 관리자 대시보드에서 여러 필드를 채워야 한다 멘티가 모른다: 새 과제가 생겨도 목록을 직접 확인하기 전까지 알 수 없다 1회성이어야 한다: 알림을 본 뒤에는 다시 보여주지 않아야 한다 설계 결정: 새 모델 vs 기존 모델 재활용 처음에는 QuickAssignment나 Notification 같은 새 모델을 만들 수 있었지만, 분석해보니 기존 구조로 충분했다. ...

2026-03-12 00:00 · 5분 소요 · Seunghan
Slack File To Activestorage Rails Seeds

Slack 파일을 Rails 프로젝트에 반영하기 — URL 저장이 아닌 소스 포함

팀원들이 Slack 채널에 HTML 파일을 과제로 제출하고 있었다. Rails 앱의 제출 상세 페이지에서 이 파일들을 인라인으로 미리보기할 수 있게 만들어야 했다. “URL 저장하면 되겠지"라는 생각으로 시작했다가 세 번의 방향 전환을 거쳤다. 1차 시도: Slack 파일 URL을 그대로 seeds에 저장 Slack의 파일 공유 URL은 이런 형태다: https://slack-files.com/T0xxx-F0xxx-hash 이걸 seeds.rb에 넣고 SlackFileImporter로 다운로드하면 ActiveStorage에 자동 첨부되는 구조가 이미 있었다. SlackFileImporter.new(submission, slack_url).call 문제: SlackFileImporter는 내부적으로 SLACK_BOT_TOKEN 환경변수를 사용한다. 배포 환경에는 토큰이 있지만, seeds가 실행되는 시점에 Slack API 호출이 실패하면 파일이 누락된다. 그리고 근본적으로 slack-files.com URL은 인증 없이 외부 웹에서 접근이 안 된다. ...

2026-03-12 00:00 · 4분 소요 · Seunghan

Flutter image_picker 카메라/갤러리 바텀시트 + Riverpod 빈도 기반 카테고리 자동 정렬 삽질기

Flutter로 시민 신고 앱을 만들면서 세 가지 UX 문제를 연달아 만났다. 사진 추가 버튼이 갤러리만 열어서 카메라 촬영이 불가능한 문제 카테고리가 늘어날수록 그리드가 길어져서 스크롤이 많아지는 문제 신고 대상(일반/긴급)이 바뀌어도 버튼 색상이 바뀌지 않아서 직관성이 떨어지는 문제 각각 어떻게 풀었는지 정리한다. 문제 1: image_picker가 갤러리만 열린다 현상 사진 추가 버튼이 pickImage(source: ImageSource.gallery)만 호출해서 카메라로 찍는 게 불가능했다. 앱 자체에 카메라 권한도 있고 NSCameraUsageDescription도 있는데 UI에서 선택지를 아예 안 줬던 것. ...

2026-03-09 00:00 · 6분 소요 · Seunghan

iOS 앱 배포 막히는 순간들 — Bundle ID 이전 불가·ITMS-90683·AI 아이콘 생성까지

Flutter 앱을 TestFlight에 올리는 과정에서 겪은 삽질들을 기록한다. Apple Developer 계정 전환, Bundle ID 등록, 권한 누락 에러, 그리고 AI로 아이콘과 스크린샷을 자동 생성하는 방법까지. 1. Apple Developer 계정이 다를 때 — Bundle ID 이전은 불가 앱을 A 계정(Team A)에서 개발하다가 B 계정(Team B)으로 배포하려고 했다. 기존 Bundle ID가 A 계정에 이미 등록되어 있어서 B 계정으로 등록하려 하면 409 Conflict 에러가 난다. { "errors": [{ "status": "409", "code": "ENTITY_ERROR.ATTRIBUTE.INVALID", "detail": "An App ID with Identifier 'com.xxx.yyy' is not available." }] } Bundle ID는 계정 간 이전이 불가능하다. 해결책은 두 가지다: ...

2026-03-08 00:00 · 4분 소요 · Seunghan
Symphony Patterns Itsm Automation

OpenAI Symphony에서 배운 7가지 패턴을 Rails ITSM에 적용한 이야기

AI 에이전트가 티켓을 잡고 방치하는 문제를 겪고 나서, OpenAI의 Symphony 프로젝트를 분석했다. Symphony는 GitHub 이슈 트래커를 폴링하고 코딩 에이전트(Codex, Claude 등)를 자동으로 실행시키는 오케스트레이터인데, 핵심 철학이 인상적이었다: “에이전트를 관리하지 말고, 일(Work)을 관리해라.” 이 철학에서 7가지 패턴을 추출하고, Rails 8 + SolidQueue 기반 ITSM 시스템에 모두 적용했다. 각 패턴이 왜 필요했는지, 어떻게 구현했는지를 실제 코드와 함께 정리한다. 배경: AI 에이전트 방치 사고 문제의 발단은 단순했다. ITSM 시스템에서 티켓을 AI 에이전트에게 배정했는데, 에이전트가 분석을 시작하고 중간에 타임아웃이 났다. 타임아웃 처리 코드가 없었기 때문에 티켓 상태는 assigned로 남았고, 시스템 어디에도 경보가 울리지 않았다. ...

2026-01-16 00:00 · 8분 소요 · Seunghan
Mcp Flutter Rails System Category Debug

MCP 도구 연동부터 Flutter 설정 토글까지 — 삽질 기록

MCP 도구로 서버 사이드에 카테고리를 생성했다. 그런데 모바일 앱에서 새 카테고리가 보이지 않았다. 간단해 보이는 문제였는데, 파고들수록 여러 레이어가 얽혀 있었다. 문제의 시작: MCP로 만든 카테고리가 앱에 안 보인다 MCP 도구를 통해 dev/, memory 같은 시스템 카테고리를 서버에 생성했다. API를 직접 호출하면 데이터가 있다. 앱을 리프레시해도 나타나지 않는다. 첫 번째 가설: 앱이 캐시를 사용하는 건가? → 아니다. PapersLoadRequested + PaperCategoriesLoadRequested 이벤트를 순서대로 디스패치하고 있었고, 서버에서 정상 응답이 오고 있었다. 두 번째 가설: API가 필터링하고 있나? → Rails 컨트롤러를 봤다. 필터 없음. 전체 반환 중. ...

2025-12-13 00:00 · 5분 소요 · Seunghan
Rails Flutter Server Health Check 4 Issues

Rails + Flutter 앱 서버 점검기: 한 번에 터진 4가지 문제와 해결

앱 테스트 빌드를 올리고 직접 돌려보니 한꺼번에 4가지가 안 됐다. Google 로그인 실패, AI 일정 생성이 엉뚱한 결과, 알림 버튼 누르면 크래시, 인기 여행지 섹션이 텅 비어있음. 하나씩 원인을 찾고 고친 과정을 정리한다. 1. Google SSO는 실패하는데 Apple 로그인은 성공 증상 Apple Sign-In은 정상 동작하지만 Google Sign-In만 500 에러. 클라이언트에서는 로그인 실패 토스트만 보인다. 원인 컨트롤러는 이전 커밋에서 수정했지만, Model의 from_omniauth 메서드는 그대로였다. # User 모델 — 마이그레이션 후에도 옛날 컬럼명 참조 def self.from_omniauth(auth) user = find_or_initialize_by(provider: auth.provider, uid: auth.uid) # uid 컬럼 없음 user.image = auth.info.image # image 컬럼도 없음 end DB 스키마에서는 uid → provider_uid, image → avatar_url로 마이그레이션된 상태. 컨트롤러 쿼리는 수정했지만 모델 내부 메서드가 여전히 옛 컬럼을 참조하고 있었다. ...

2025-10-15 00:00 · 5분 소요 · Seunghan
개인정보처리방침 이용약관 면책조항 문의