포스트

[AX] 파트너스 매칭 매니저 v5.1 — 급여 산식 정밀화 및 모바일 카드 레이아웃

70명 이상의 프리랜서를 관리하는 급여 시스템에 역할수당·현장수당 분리 산식과 모바일 카드 레이아웃을 적용한 v5.1 업그레이드 전 과정을 공개합니다.

[AX] 파트너스 매칭 매니저 v5.1 — 급여 산식 정밀화 및 모바일 카드 레이아웃

🏷️ [NextX_AX_Solution] · 주식회사 넥스트엑스(NEXT X) AX 솔루션 운영·유지보수 기록

이 글은 파트너스 매칭 매니저 시리즈의 열 번째 글입니다.

  1. 프로토타입 제작기 — MVP 개발
  2. 실전 납품 개발기 — 인증·보안·실데이터
  3. Auth 트러블슈팅 — 로그인 오류 해결
  4. v2 업그레이드 — 명부 교체·스키마 유연화
  5. v3 업그레이드 — 팀 배정 시스템·캘린더 뷰
  6. v3.1 업그레이드 — 휴무일 관리·스케줄 충돌 방지
  7. v4 업그레이드 — 급여 정산 및 관리 시스템
  8. v4.1 업그레이드 — UX 고도화 및 급여 기타수당
  9. v5 업그레이드 — 통합 일정 관리 달력
  10. [현재 글] v5.1 업그레이드 — 급여 산식 정밀화 및 모바일 카드 레이아웃
  11. v5.2 업그레이드 — 엑셀 기반 Mock 데이터 파이프라인

📋 업그레이드 배경

70명의 급여를 실시간으로 정산하려면

v4.1에서 급여 관리 시스템을 도입하면서 기본 시급 × 근무 시간 + 기타수당 산식으로 정산을 시작했습니다. 하지만 실제 현장 운영에서 두 가지 한계가 드러났습니다:

문제현재 (v4.1)실제 필요
수당 구분“기타수당” 하나에 모든 수당 합산팀장수당(역할수당)과 현장수당을 분리
역할별 차등팀장/팀원 구분 없이 동일 입력팀장은 팀장수당 항목이 강조
모바일 조회5열 테이블이 모바일에서 가로 스크롤모바일에서 카드 형태로 조회

핵심 요구사항

  1. 급여 산식 정밀화(기본 시급 × 근무시간) + 역할수당(팀장수당) + 현장이동/초과근무수당
  2. 모바일 반응형 카드 레이아웃 — PC에서는 테이블, 모바일(<640px)에서는 카드

💰 Phase 1 — 급여 산식 정밀화

DB 스키마 확장

기존 payroll_records 테이블에 field_bonus 컬럼을 추가합니다:

1
2
ALTER TABLE payroll_records
  ADD COLUMN IF NOT EXISTS field_bonus INTEGER DEFAULT 0;
컬럼용도비고
hourly_rate기본 시급기존
hours_worked근무 시간기존
bonus역할수당 (팀장수당)기존 “기타수당”에서 역할 재정의
field_bonus현장수당 (이동/초과근무)신규
total_amount총 급여산식 변경

기존 bonus 컬럼의 역할을 “기타수당”에서 “역할수당”으로 재정의합니다. 기존 데이터는 그대로 유지되며, 새 field_bonus 컬럼은 DEFAULT 0이므로 기존 레코드에 영향을 주지 않습니다.

급여 산식 변경

flowchart LR
    subgraph "v4.1 산식"
        A1["시급 × 시간"] --> B1["+"]
        C1["기타수당"] --> B1
        B1 --> D1["총액"]
    end

    subgraph "v5.1 산식"
        A2["시급 × 시간"] --> B2["+"]
        C2["역할수당<br/>(팀장수당)"] --> B2
        E2["현장수당<br/>(이동/초과)"] --> B2
        B2 --> D2["총액"]
    end

JavaScript 계산 함수의 변경:

1
2
3
4
5
6
7
// v4.1 — 기타수당 하나
const total = Math.round(rate * hours) + Math.round(bonus);

// v5.1 — 역할수당 + 현장수당 분리
const total = Math.round(rate * hours)
            + Math.round(roleBonus)
            + Math.round(fieldBonus);

워커 행 UI 개선

각 근무자 행에 수당 입력 필드를 추가하고, 팀장과 팀원을 시각적으로 구분합니다:

flowchart TB
    subgraph "근무자 행 (v5.1)"
        direction LR
        A["👑/👤 이름"] --- B["시급 × 시간"]
        B --- C["+ 역할수당"]
        C --- D["+ 현장수당"]
        D --- E["= 총액"]
    end

    style C fill:#fef3c7,stroke:#f59e0b,color:#92400e

팀장의 역할수당 입력란에는 amber 배경색(bg-amber-50 border-amber-200)을 적용해서 시각적으로 구분합니다:

1
2
3
4
5
6
const roleBonusLabel = isLeader ? '팀장수당' : '역할수당';

// 팀장인 경우 amber 배경 강조
`<input class="payroll-bonus ...
  ${isLeader ? 'bg-amber-50 border-amber-200' : ''}"
  placeholder="${roleBonusLabel}" />`

저장 로직 확장

savePayrollForAssignment() 함수에 field_bonus 필드를 추가합니다:

1
2
3
4
5
6
7
8
9
10
11
12
records.push({
  assignment_id: assignmentId,
  partner_id: partnerId,
  hourly_rate: Math.round(rate),
  hours_worked: hours,
  bonus: Math.round(roleBonus),        // 역할수당
  field_bonus: Math.round(fieldBonus),  // 현장수당 (신규)
  total_amount: Math.round(rate * hours)
              + Math.round(roleBonus)
              + Math.round(fieldBonus),
  work_date: assignment.assignment_date,
});

📱 Phase 2 — 모바일 반응형 카드 레이아웃

문제: 5열 테이블의 모바일 가독성

급여 통계 섹션의 파트너별 명세 테이블은 5개 컬럼(파트너, 근무 건수, 총 근무시간, 평균 시급, 총 급여)으로 구성됩니다. PC에서는 문제없지만, 모바일 화면에서는 가로 스크롤이 필요하거나 텍스트가 잘리는 문제가 발생합니다.

해결: Tailwind 반응형 듀얼 렌더링

하나의 데이터를 두 가지 포맷으로 동시에 렌더링하되, Tailwind의 반응형 클래스로 화면 크기에 따라 적절한 것만 보여줍니다:

flowchart TD
    A["renderPayrollDashboard()"] --> B["byPartner 데이터 집계"]
    B --> C["PC 테이블 렌더링<br/>hidden sm:block"]
    B --> D["모바일 카드 렌더링<br/>sm:hidden"]
    C --> E["640px 이상: 테이블 표시"]
    D --> F["640px 미만: 카드 표시"]

HTML 구조

1
2
3
4
5
6
7
8
9
10
11
12
<!-- PC 테이블: 640px 이상에서만 표시 -->
<div class="overflow-x-auto hidden sm:block">
  <table class="w-full text-sm">
    <thead>...</thead>
    <tbody id="payroll-stat-table">...</tbody>
  </table>
</div>

<!-- 모바일 카드: 640px 미만에서만 표시 -->
<div id="payroll-stat-cards" class="sm:hidden space-y-3">
  ...
</div>
클래스의미적용 대상
hidden sm:block기본 숨김, 640px+ 표시PC 테이블
sm:hidden기본 표시, 640px+ 숨김모바일 카드

카드 레이아웃 구조

모바일 카드는 2열 그리드로 핵심 정보를 압축합니다:

flowchart TB
    subgraph "모바일 카드 1장"
        H["파트너명 ←→ 총 급여"]
        G1["근무 건수 | 총 근무시간"]
        G2["평균 시급 | 기본급"]
        G3["역할수당 (amber) | 현장수당 (blue)"]
    end
    H --> G1 --> G2 --> G3

수당 항목은 0원이 아닌 경우에만 조건부 렌더링됩니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
${data.totalRoleBonus > 0 ? `
<div class="flex justify-between bg-amber-50 rounded-lg px-3 py-2">
  <span class="text-amber-600">역할수당</span>
  <span class="font-semibold text-amber-700">
    ₩${data.totalRoleBonus.toLocaleString()}
  </span>
</div>` : ''}

${data.totalFieldBonus > 0 ? `
<div class="flex justify-between bg-blue-50 rounded-lg px-3 py-2">
  <span class="text-blue-600">현장수당</span>
  <span class="font-semibold text-blue-700">
    ₩${data.totalFieldBonus.toLocaleString()}
  </span>
</div>` : ''}

조건부 렌더링으로 수당이 없는 파트너의 카드는 간결하게, 수당이 있는 파트너의 카드는 상세하게 표시됩니다. 정보 밀도를 데이터에 맞게 동적으로 조절하는 것이 모바일 UX의 핵심입니다.


📐 변경 사항 요약

버전별 비교

항목v4.1v5.1
급여 산식시급×시간 + 기타수당시급×시간 + 역할수당 + 현장수당
수당 분류1종 (기타수당)2종 (역할 + 현장)
팀장 시각 구분👑 배지만👑 배지 + amber 입력란
급여 통계 (PC)5열 테이블5열 테이블 (유지)
급여 통계 (모바일)가로 스크롤 필요카드 레이아웃
DB 변경없음field_bonus 컬럼 추가
카드 수당 표시조건부 (0원이면 숨김)

변경 파일

파일변경 내용
index.html급여 통계 영역 듀얼 렌더링 구조 (hidden sm:block + sm:hidden)
src/main.js워커 행 현장수당 필드, calcPayrollRow() 산식, savePayrollForAssignment() 저장 로직, renderPayrollDashboard() 카드 렌더링
DBpayroll_records.field_bonus INTEGER DEFAULT 0 추가

🧮 데이터 흐름

v5.1에서 변경된 급여 계산 및 렌더링 파이프라인:

flowchart TD
    subgraph "입력 (배정별 급여 입력)"
        I1["시급 input<br/>.payroll-rate"]
        I2["시간 input<br/>.payroll-hours"]
        I3["역할수당 input<br/>.payroll-bonus"]
        I4["현장수당 input<br/>.payroll-field-bonus"]
    end

    subgraph "계산 (실시간)"
        C["calcPayrollRow()"]
        F["(rate × hours)<br/>+ roleBonus<br/>+ fieldBonus"]
    end

    subgraph "저장"
        S["savePayrollForAssignment()"]
        DB["payroll_records<br/>bonus + field_bonus"]
    end

    subgraph "조회 (급여 통계)"
        R["renderPayrollDashboard()"]
        T["PC: 테이블<br/>hidden sm:block"]
        M["Mobile: 카드<br/>sm:hidden"]
    end

    I1 & I2 & I3 & I4 --> C --> F
    F -->|"oninput"| C
    F -->|"💾 저장 버튼"| S --> DB
    DB -->|"월별 조회"| R
    R --> T
    R --> M

🚀 배포

v5.1 배포 프로세스

flowchart LR
    A["1️⃣ DB 마이그레이션<br/>field_bonus 컬럼"] --> B["2️⃣ index.html 수정<br/>듀얼 레이아웃"]
    B --> C["3️⃣ main.js 수정<br/>산식 + 카드 렌더링"]
    C --> D["4️⃣ Vite 빌드<br/>258KB 번들"]
    D --> E["5️⃣ Git Push"]
    E --> F["6️⃣ Vercel 배포"]
    F --> G["✅ v5.1 Live"]

ALTER TABLE ... ADD COLUMN IF NOT EXISTS를 사용하므로 마이그레이션은 멱등적(idempotent)입니다. 여러 번 실행해도 안전합니다. DEFAULT 0 덕분에 기존 레코드는 자동으로 field_bonus = 0을 갖게 됩니다.


💡 실전에서 배운 것

1. 기존 컬럼의 역할 재정의 vs 새 컬럼 추가

접근적용이유
bonus → “역할수당”역할 재정의기존 데이터 호환, 코드 변경 최소화
field_bonus 추가새 컬럼새로운 의미의 데이터, DEFAULT 0으로 안전

“기타수당”이라는 모호한 이름을 “역할수당”으로 명확히 하면서, 실제로는 기존 데이터나 컬럼을 변경하지 않습니다. 이름만 바꾸고 새 필드를 추가하는 것이 가장 안전한 스키마 진화 패턴입니다.

2. Tailwind 반응형 듀얼 렌더링의 트레이드오프

1
2
3
// 같은 데이터를 두 번 렌더링하는 비용
tableBody.innerHTML = entries.map(/* 테이블 행 */).join('');
cardContainer.innerHTML = entries.map(/* 카드 *//).join('');
장점단점
CSS 미디어쿼리만으로 전환DOM 노드 2배
JS 리사이즈 이벤트 불필요렌더링 함수 코드 증가
깜빡임 없는 즉시 전환

70명 규모에서 DOM 노드 2배는 무시 가능한 수준입니다. 수천 건 이상이면 matchMedia API로 한쪽만 렌더링하는 최적화가 필요하지만, 현재 규모에서는 과도한 설계입니다.

3. 조건부 카드 항목의 UX 효과

1
2
3
// 수당이 0이면 카드에서 아예 안 보임
${data.totalRoleBonus > 0 ? `<div>역할수당: ₩...</div>` : ''}
${data.totalFieldBonus > 0 ? `<div>현장수당: ₩...</div>` : ''}

모든 카드에 모든 항목을 동일하게 표시하면, 수당이 없는 파트너의 카드에 ₩0이 두 줄이나 차지합니다. 의미 있는 정보만 표시하면:

  • 수당 없는 일반 팀원 카드: 4줄 (간결)
  • 수당 있는 팀장 카드: 6줄 (상세)

정보 밀도가 데이터에 따라 자연스럽게 조절됩니다.


📈 시리즈 타임라인

gantt
    title 파트너스 매칭 매니저 전체 개발 일정
    dateFormat YYYY-MM-DD
    axisFormat %m/%d

    section Phase 1 — MVP
    DB 설계 + 프론트엔드      :2026-07-10, 3d
    GitHub Pages 배포        :2026-07-12, 1d

    section Phase 2 — Production
    관리자 인증 (Auth)        :2026-07-15, 2d
    RLS 보안 강화            :2026-07-16, 1d
    회사 브랜딩 + 대시보드     :2026-07-16, 1d
    실데이터 연동 (57명)      :2026-07-17, 1d
    Auth 트러블슈팅           :2026-07-17, 1d

    section Phase 3 — v2 Upgrade
    스키마 유연화             :2026-07-18, 1d
    명부 교체 (71명)          :2026-07-18, 1d

    section Phase 4 — v3 Upgrade
    캘린더 뷰 구현            :2026-07-18, 1d
    팀 배정 스키마 마이그레이션 :2026-07-18, 1d

    section Phase 5 — v3.1 Upgrade
    partner_day_offs 테이블   :2026-07-19, 1d
    휴무 등록 + 달력 + 충돌방지 :2026-07-19, 1d

    section Phase 6 — v4 Upgrade
    payroll_records 테이블    :2026-07-19, 1d
    급여 입력 UI + 실시간 계산  :2026-07-19, 1d
    급여 통계 대시보드          :2026-07-19, 1d

    section Phase 7 — v4.1 Enhancement
    전화번호 자동 포맷팅        :2026-07-19, 1d
    파트너 인라인 수정          :2026-07-19, 1d
    급여 기타수당 필드          :2026-07-19, 1d

    section Phase 8 — v5 Schedule
    탭 + 패널 UI 추가          :2026-07-19, 1d
    통합 달력 렌더링 로직        :2026-07-19, 1d
    클릭 이벤트 연결            :2026-07-19, 1d

    section Phase 9 — v5.1 Payroll+Mobile
    급여 산식 정밀화 (역할+현장)  :2026-07-19, 1d
    모바일 카드 레이아웃         :2026-07-19, 1d
    프로덕션 반영             :milestone, 2026-07-19, 0d

🔗 프로젝트 링크


🔮 다음 단계

v5.1까지 완료된 시스템의 현재 상태와 앞으로의 계획:

기능상태다음 목표
파트너 CRUD + 인라인 수정일괄 수정 (복수 파트너)
관리자 인증 + RLS다중 관리자 권한 분리
캘린더 뷰 + 통합 일정주간 뷰, 일간 상세 뷰
팀 배정 + 휴무 관리정기 휴무 패턴 자동 등록
급여 정산 (역할+현장수당)PDF/Excel 내보내기
급여 통계 + 모바일 카드분기별·연간 급여 추이 차트
AI 자동 매칭🔜지역·전문성·휴무·과거 이력 기반 추천

v5.1은 “같은 데이터를 더 정확하게 입력하고, 더 편리하게 조회하는 것”에 집중한 업그레이드입니다. 산식 정밀화로 70명 이상의 프리랜서를 역할별·수당별로 정확히 정산할 수 있게 되었고, 모바일 카드 레이아웃으로 현장에서도 급여 현황을 한눈에 확인할 수 있게 되었습니다.


NEXT X R&D · AI Transformation

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.