← 프로젝트 프로젝트 케이스 스터디

p4-web

p4 CLI를 감싼 단일 프로세스로 P4V의 일상 워크플로를 브라우저에 옮긴 웹 퍼포스 클라이언트입니다

체인지리스트 목록 화면입니다. 사용자·경로·텍스트·날짜 필터와 함께 검색 인덱스 상태 바가 표시되어 있습니다.
역할
1인 개발 — 기획·설계·구현·보안 감사 전반
기간
2026년 7월, 약 2일 집중 개발
스택
Python 3.10+ · FastAPI · p4 CLI(-G 마샬) · 바닐라 JS SPA(빌드 스텝 없음) · SQLite(세션·검색 인덱스) · highlight.js

P4V는 데스크톱 설치가 필요하고 Swarm은 개인이 운영하기엔 무겁습니다. 그 사이에서 개인과 소규모 팀 크기에 맞는 웹 퍼포스 클라이언트를 목표로 했습니다. 구조 원칙은 단순함입니다. 데이터베이스 서버도 빌드 스텝도 외부 서비스도 없이 p4 커맨드라인 클라이언트를 셸로 감싸는 FastAPI 프로세스 하나와 프레임워크 없는 해시 라우팅 SPA가 전부입니다. 로그인하면 패스워드를 p4 login 티켓으로 교환하고 패스워드 자체는 저장하지 않습니다. 이후 모든 API 호출이 그 세션 사용자의 티켓으로 실행되므로 퍼포스 protections가 P4V에서와 똑같이 적용됩니다.

기능 폭은 P4V의 일상 워크플로를 기준으로 잡았습니다. 지연 로딩 디팟 트리와 문법 강조 파일 뷰어, 리네임·브랜치를 따라가는 히스토리, 어디서나 통하는 Unified/Split diff, 임의 리비전 쌍 비교와 디렉토리 diff, 리비전 슬라이더로 과거 시점 블레임을 다시 계산하는 타임랩스 어노테이트, 인테그레이션 SVG 그래프, 파일명·내용 검색, 라벨·잡·브랜치·스트림·사용자 메타데이터 브라우저까지 조회 계열을 먼저 완성했습니다. 768px 아래에서는 트리가 슬라이드오버로 바뀌는 모바일 레이아웃과 다크 모드도 갖췄습니다.

체인지리스트 검색에는 사용자별 SQLite 인덱스를 씁니다. 여기엔 두 가지 결정이 있습니다. 첫째로 인덱스 빌드는 p4 describe -s가 복수 체인지 번호를 받는다는 점을 이용해 500건 배치당 p4 호출 2번으로 줄였습니다(체인지 1,000건·파일 8천 개에 약 20초). 둘째로 인덱스를 공유하지 않고 요청 사용자의 티켓으로 각자 빌드합니다. restricted 체인지가 남에게 유출되지 않도록 권한 검사를 재구현하는 대신 애초에 “그 사용자가 볼 수 있는 것”만 인덱스에 들어가게 한 것입니다. 검색 엔진 선택 토글은 두지 않았습니다. 펜딩 상태나 경로 프리픽스처럼 인덱스가 못 하는 조건이면 라이브 p4로, 그 외에는 인덱스로 자동 판별합니다.

쓰기 지원이 이 프로젝트에서 가장 조심스러운 부분이었습니다. 서버가 사용자 로컬 디스크를 볼 수 없으므로 사용자별 영구 서버측 워크스페이스(p4web-<user>)를 두고 웹 에디터 편집·파일 업로드·삭제 마킹·리버트·셸브·서브밋을 그 안에서 처리합니다. 안전장치는 세 겹입니다. 본인 소유·펜딩·본인 클라이언트 세 조건을 모두 확인해야 쓰기가 허용되고, 서브밋은 확인 다이얼로그를 거치고, 모든 쓰기가 감사 로그에 남습니다. 구현 과정에서 p4의 경고 수준 실패가 예외를 던지지 않는다는 것을 여러 번 겪었습니다. .p4ignore가 add를 조용히 거부하고 p4 unshelve는 열린 파일 위를 경고로 건너뜁니다. 그래서 쓰기 명령은 “명령이 성공했다”가 아니라 p4 opened류의 사후 확인으로 “원하는 상태가 됐다”를 검증합니다.

공개 전에 전 라우트 공격면을 열거하는 코드리뷰와 보안 감사, 실 서버 기동 레드팀을 거쳤습니다. 자격증명 격리와 쓰기 인가, 경로 탈출, XSS, 명령 인젝션의 핵심 경계는 건전함을 확인했고 Content-Disposition 헤더 살균 같은 방어 심화 3건을 찾아 즉시 수정했습니다. 디팟 컨텐츠는 신뢰할 수 없는 입력으로 취급해 마크다운은 DOMPurify를 거치고 raw 응답은 CSP sandbox로 격리합니다. 읽기 명령 화이트리스트와 로그인 레이트리밋도 둡니다. 운영은 launchd·systemd·도커 세 갈래를 준비했고 홈랩에서는 launchd 상주와 Tailscale Serve로 테일넷에만 노출합니다. 세션은 SQLite로 영속화해 재시작해도 로그인이 유지됩니다.

← 프로젝트로 돌아가기