[병원 휴가관리 시스템 #1] 병원 직원들의 연차, 휴일관리 시스템 구축

ClaudeCode로 병원 직원 연차·휴일 관리 시스템 처음부터 만들기

시작하게 된 계기

소규모 병원에서 일하는 지인은 직원 연차 관리가 늘 골칫거리였습니다. 직원이 15명 안팎인데도 구두로 "저 다음 주 화요일에 쉬어도 돼요?"라고 하거나 캘린더에 적어두는 방식으로 관리하다 보니 같은 팀 직원 둘이 같은 날 쉬어버리는 상황이 종종 생겼습니다.

엑셀 파일로 옮겨봤지만, 결국 종이로 된 캘린더에 적던 것을 엑셀에 적는다는 것 외에는 별반 차이가 없었습니다. 무엇보다 각 직원의 연차가 며칠이 남았는지를 알고 싶었고, 또 누가 언제 쉬는지 달력으로 보고 싶고, 팀별로 같은 날 두 명이 겹치면 자동으로 막아줬으면 했습니다. "이 정도면 시스템을 만들어야겠다"고 마음먹었지만, 비개발자인 지인으로선 어디서 시작해야 할지 막막하여 저에게 고민을 털어놓았습니다. 이 말을 들은 저는 AI의 힘을 빌리면 가능하겠다는 생각을 했습니다. 그러나 저도 비개발자여서 실제로 AI를 활용할 시도는 하지 못한 채 시간은 흘렀고, GPTers에 들어오게 되었습니다.

GPTERs에서 이재엽 스터디장님의 개발환경셋팅 강의와 그 밖의 여러가지 미니강의 세션들을 통해 Git, Antigravity, WSL, Claude Code 등을 알게 됐습니다. 또한 청강했던 스타트업실험실, 그리고 소설임팩트 강의를 통해 바이브코딩에 대해 접해볼 수 있었습니다. 스터디장님들과 다른 선배님들의 시연 및 사용기를 보고, 클로드 코드와 안티그래비티 등을 직접 써보니 "이런 기능이 필요해"라고 설명하면 파일 구조부터 API, 화면까지 한 번에 만들어주는 수준이었습니다. 그래서 약 2주 만에 실제로 쓸 수 있는 웹 시스템을 완성한 과정을 공유합니다.


🛠️ 사용한 도구

  • Claude Code — 코드 설계·구현·디버깅 전 과정

  • Next.js 14 (App Router) — 웹 프레임워크 (프론트엔드 + API 라우트 통합)

  • TypeScript — 타입 안전성 확보

  • Tailwind CSS — 반응형 UI 스타일링

  • NextAuth.js v4 — 로그인·JWT 세션 인증

  • Airtable — 데이터베이스 (별도 서버 없이 관리자가 직접 확인 가능)

  • react-calendar — 커스텀 캘린더 UI

  • date-fns — 날짜 연산 유틸리티

  • Vercel — 배포 (CLI 명령어 vercel --prod 한 줄)


1단계: 업무 흐름을 그대로 던지기

개발 용어 없이 실제 병원 운영 방식 그대로 설명했습니다.

"직원들이 직접 연차를 신청하고, 관리자가 확인할 수 있어야 해. 같은 팀에 있는 직원끼리는 같은 날 쉬면 안 돼. 연차 말고 주간 휴일이라는 제도도 있는데, 매주 1회씩 쉴 수 있고 그 주에 안 쓰면 소멸돼(이것은 나중에 지인과 이야기해서 4주동안 쓸 수 있도록 수정했습니다). 달력에서 누가 언제 쉬는지 한눈에 보이면 좋겠어."

Claude Code는 이 설명에서 핵심 규칙들을 바로 뽑아냈습니다. 여기에는 여행가J님의 스타트업실험실(청강) 강의의 도움을 받았습니다.

도출된 비즈니스 규칙

구분

규칙

연차

전일(1일) / 오전(0.5일) / 오후(0.5일) 선택

휴일

매주 1회 부여, 주 내 미사용 시 자동 소멸

제한

같은 팀 동일 날짜·시간대 중복 불가

제한

병원 전체 동시 휴무 최대 N명 (관리자 설정)

예외

일요일·공휴일 신청 불가

그 다음 Claude Code가 전체 데이터 모델을 설계했습니다. Airtable 6개 테이블, Next.js API 라우트 목록, 화면 구성까지 한 번에 정리됐습니다.


2단계: 시간대 충돌 판정 — 핵심 로직 설계

가장 어려운 부분이 "오전 반차 신청자가 있을 때 오후 반차는 괜찮은가?"처럼 시간대별 충돌을 판정하는 로직이었습니다. Claude Code가 아래 행렬로 정리해줬습니다.

기존 신청

신규 신청

오전 충돌

오후 충돌

전일 연차

전일 연차

O

O

전일 연차

오전 연차

O

-

전일 연차

오후 연차

-

O

오전 연차

오후 연차

-

-

휴일(전일)

오전 연차

O

-

휴일(전일)

오후 연차

-

O

이 행렬을 코드로 표현하기 위해 getSlotCoverage() 함수 하나로 추상화했습니다.

// 신청 유형에 따라 점유하는 시간대 반환
export function getSlotCoverage(
  type: 'full' | 'am' | 'pm' | 'dayoff'
): ('am' | 'pm')[] {
  if (type === 'full' || type === 'dayoff') return ['am', 'pm'];
  if (type === 'am') return ['am'];
  if (type === 'pm') return ['pm'];
  return [];
}

충돌 판정은 이 함수를 루프 돌리면 됩니다.

const newSlots = getSlotCoverage(type); // 신규 신청 시간대

for (const slot of newSlots) {
  for (const existingLeave of leavesOnDate) {
    const coverage = getSlotCoverage(existingLeave.type);
    if (coverage.includes(slot)) {
      return error('같은 시간대에 이미 신청이 있습니다');
    }
  }
}

복잡해 보이는 충돌 판정이 함수 하나와 이중 루프로 깔끔하게 정리됐습니다.


3단계: Airtable 구조 설계

서버를 직접 운영하지 않으면서 관리자가 데이터를 직접 확인하고 싶었기 때문에 Airtable을 선택했습니다. Claude Code가 6개 테이블을 설계했습니다. Airtable 선택에는 닿님의 소셜임팩트 강의를 청강한 것이 큰 도움이 되었습니다.

Teams          팀명, 소속 직원
Employees      직원 정보, 비밀번호 해시, 연차 잔여일
LeaveRequests  연차 신청 내역 (날짜, 유형, 상태)
WeeklyDayOff   주간 휴일 신청 내역 (주 시작일 포함)
Holidays       공휴일 목록 (연도별)
Settings       시스템 설정 (동시 휴무 최대 인원 등)

설계 포인트: 연차 잔여일수 계산을 Airtable 자체 Formula 필드 대신 API에서 직접 관리하도록 했습니다. 연차 신청 시 UsedLeave +1, 취소 시 -1을 코드에서 명시적으로 처리합니다. Airtable Formula는 나중에 동기화 문제를 일으킬 수 있기 때문입니다.

사실 이 Airtable에 대해서, Claude Code에서는 저보고 직접 입력하도록 했습니다. 그러나 저는 클로드 코드에게 "네가 할 수 있는 일은 네가 하고, 네가 할 수 없는 일만 내가 하겠다"고 했습니다. 다행히 클로드 코드는 저의 의도를 잘 알아듣고 Airtable에 필드를 입력하는 것까지 직접 수행했습니다. 제가 직접 Airtable에 들어가서 한 일은 레코드 생성과 API Key(token?) 발급한 것 외에는 거의 없었습니다.


4단계: 인증 시스템 — NextAuth + Airtable 직접 연동

별도 인증 서버 없이 Airtable의 Employees 테이블을 직접 인증 소스로 사용했습니다. 비밀번호는 bcrypt로 해싱해서 저장합니다.

// NextAuth CredentialsProvider
async authorize(credentials) {
  const employee = await getEmployeeByLoginId(credentials.loginId);

  // 미승인(pending) 계정은 로그인 차단
  if (!employee || employee.status !== 'active') return null;

  const isValid = await bcrypt.compare(
    credentials.password,
    employee.passwordHash
  );
  if (!isValid) return null;

  return {
    id: employee.id,
    name: employee.name,
    role: employee.role,    // 'admin' | 'staff'
    teamId: employee.teamId,
  };
}

JWT 세션에 id, role, teamId를 담아서 모든 API 라우트에서 권한 체크에 활용합니다. 세션 유효시간은 병원 근무 시간을 고려해 8시간으로 설정했습니다.


5단계: 아키텍처 — 3계층 구조

┌─────────────────────────────────────┐
│    프론트엔드 (Next.js React)        │
│  로그인 · 대시보드 · 캘린더 · 관리자  │
└──────────────┬──────────────────────┘
               │ HTTP (fetch)
┌──────────────▼──────────────────────┐
│   API 계층 (Next.js Route Handlers) │
│  인증 · 휴가 · 휴일 · 관리자 API     │
│  동시 휴무 판정 · 권한 체크          │
└──────────────┬──────────────────────┘
               │ Airtable REST API
┌──────────────▼──────────────────────┐
│    데이터 계층 (Airtable)            │
│  6개 테이블 · 설정 · 공휴일          │
└─────────────────────────────────────┘

Next.js App Router 덕분에 프론트엔드와 API를 하나의 프로젝트에서 관리하면서 Vercel에 배포할 수 있었습니다. 별도 백엔드 서버가 필요 없습니다.

모든 Airtable 접근 함수를 src/lib/airtable.ts 한 파일에 중앙화한 것이 핵심 설계 결정이었습니다. API 라우트 파일들이 DB 조작 없이 비즈니스 로직에만 집중할 수 있어 코드가 훨씬 읽기 쉬워졌습니다.


6단계: 빌드 시 발생한 이슈 해결

초기 구현 후 npm run build를 실행하니 오류가 났습니다.

이슈: 빌드 타임 Airtable 초기화 오류

Error: AIRTABLE_API_KEY is required

원인: 모듈 로드 시점에 Airtable 클라이언트를 생성하면 빌드 환경에 환경변수가 없어서 실패합니다.

해결: Lazy initialization 패턴 — 클라이언트를 함수 호출 시점에 생성합니다.

// ❌ 빌드 실패 - 모듈 로드 시점에 환경변수 필요
const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(...)

// ✅ 런타임에만 생성 - 빌드 통과
function table(tableName: string) {
  return new Airtable({ apiKey: process.env.AIRTABLE_API_KEY })
    .base(process.env.AIRTABLE_BASE_ID!)(tableName);
}

이 패턴 하나로 빌드 이슈가 해결됐고, 이후 모든 DB 함수에 동일하게 적용했습니다.


7단계: 화면 구성 — 모바일 우선 반응형

병원 특성상 직원들이 PC보다 휴대폰으로 쓸 가능성이 높아 모바일 화면을 우선으로 설계했습니다.

네비게이션 전략

  • 모바일 (md 미만): 하단 고정 탭 바 4개 버튼

  • PC/태블릿 (md 이상): 왼쪽 사이드바

{/* PC/태블릿 전용 사이드바 */}
<aside className="hidden md:block w-56 ...">

{/* 모바일 전용 하단 탭 */}
<nav className="fixed bottom-0 md:hidden ...">

구현된 화면 목록

화면

주요 기능

로그인

아이디/비밀번호 인증

대시보드

연차 현황 카드 3개 + 예정 일정 목록

연차 신청

날짜·유형 선택 + 신청 가능 여부 실시간 확인

내 연차

신청 내역 + 취소 버튼

휴일 신청

이번 주 사용 여부 확인 후 신청

내 휴일

신청 내역 + 상태 뱃지

전체 캘린더

월별 캘린더 + 날짜 클릭 시 상세 팝업

관리자 대시보드

오늘 현황 요약 + 빠른 링크

직원·팀·공휴일·설정 관리

CRUD

캘린더 색상 코딩

const colorMap = {
  leave_full:  'bg-blue-500',    // 전일 연차 — 파랑
  leave_am:    'bg-sky-400',     // 오전 연차 — 스카이
  leave_pm:    'bg-indigo-500',  // 오후 연차 — 인디고
  dayoff:      'bg-green-500',   // 주간 휴일 — 초록
  holiday:     'bg-red-500',     // 공휴일 — 빨강
};

8단계: 최종 파일 구조

약 2주간 작업 결과, 48개 파일로 구성된 시스템이 완성됐습니다.

clinic/
└── src/
    ├── app/
    │   ├── layout.tsx / page.tsx / globals.css
    │   ├── login / dashboard / calendar
    │   ├── leave/  request / my
    │   ├── dayoff/ request / my
    │   ├── admin/  (대시보드, 직원, 팀, 공휴일, 설정)
    │   └── api/
    │       ├── auth/[...nextauth]
    │       ├── leave/  (request, cancel, my, calendar, check)
    │       ├── dayoff/ (request, cancel, my, status)
    │       └── admin/  (employees, teams, holidays, settings, dashboard, reset-year)
    ├── components/
    │   ├── layout/ (AppLayout, Navbar, Sidebar, BottomTabBar)
    │   ├── calendar/ CalendarView.tsx
    │   └── ui/ (Button, Badge, Modal)
    ├── lib/
    │   ├── airtable.ts   ← 모든 DB 함수 중앙화
    │   └── auth.ts       ← NextAuth 설정
    └── types/
        └── index.ts      ← 전체 TypeScript 인터페이스

✅ 결과 (After)

항목

이전

이후

연차 신청 방식

구두/메모

직원 직접 웹 신청

팀 충돌 방지

관리자가 일일이 확인

시스템 자동 차단

현황 파악

엑셀/기억 의존

실시간 캘린더 + 대시보드

휴일 관리

별도 추적 없음

주 단위 자동 적립·소멸

모바일 접근

불가

모바일 최적화 반응형

개발 기간

약 2주 (비개발자)

배포 방법

vercel --prod


그 결과 구축된 페이지는 다음과 같습니다.

한국 �넷플릭스 한국판 로그인 페이지

관리자 아이디와 비밀번호를 입력하면,

�한국어가 포함된 한국어 앱 스크린샷

이렇게 관리자 명의로 병원 연차 관리 시스템에 접속하게 됩니다.

이것으로 끝일까요? 아닙니다. 실제로 개발 기획서에서 정한 방식이 제대로 구현되는지, 혹시 빠뜨리는 점은 없는지에 대해 세세한 조정 작업이 필요했습니다.

이제 실제로 베타테스트를 해보면서 기능을 시험해보겠습니다.

컴퓨터 화면에 한국어 스크린샷

느낀 점

처음에 걱정했던 것은 "코딩을 모르는 내가 이 복잡한 시스템을 만들 수 있을까?"였습니다. 특히 시간대별 충돌 판정 같은 로직이 막막했는데, 업무 규칙을 설명하니 Claude Code가 행렬로 정리하고 코드로 구현해줬습니다.

가장 도움이 된 방법: 기술 용어보다 실제 업무 상황을 설명하는 것이 훨씬 효과적이었습니다. "같은 팀 직원이 같은 날 쉬면 안 돼"라는 말 한 마디로 팀별 충돌 체크 로직 전체가 만들어졌습니다.

예상보다 빠른 것: 반복 작업. "이 페이지랑 비슷하게 다른 페이지도 만들어줘"라고 하면 기존 패턴을 그대로 복제해 새 화면을 뚝딱 만들어냅니다.

주의할 것: 외부 서비스(Airtable)의 미묘한 동작 방식은 문서를 봐도 실제로 써봐야 알 수 있는 부분이 있습니다. 이 시스템을 실제로 운영하면서 발견된 Airtable의 날짜 비교 연산자 문제, ARRAYJOIN 버그, 팀 충돌 메시지 오류 등은 사용해보고 나서야 드러났습니다. 초기 구현을 완성하고 나서 실제로 써보는 과정이 반드시 필요합니다.

비개발자도 업무 흐름을 구체적으로 설명할 수 있다면 Claude Code로 충분히 실무 수준의 시스템을 만들 수 있습니다.

의뢰한 지인의 반응: 구축된 웹페이지를 보고도 반신반의했습니다. 레이아웃에 대해서는 '전문가가 만든 것 같다'고 하면서도, 과연 이것을 실제 써먹을 수 있는지에 대해서는 상당히 의구심을 갖고 있었습니다. 생각해보니 GPTers 배운다고 시작한 지가 엊그제인데, 갑자기 어느 날 웹페이지를 만들어서 그것이 컴퓨터나 모바일에서 작동된다고 하니 잘 받아들이지 못한 것 같습니다. 4월 중 충분한 베타테스트를 거쳐서 병원에서 사용할 예정입니다.


참고

1
2개의 답글

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요