본문 바로가기

웹앱 공통 컴포넌트

웹앱(app/) 전용 공통 스타일입니다. 수정은 app/css/common.css (토큰 · 타이포그래피 · 컴포넌트) / app/css/layout.css (헤더 · 탭바 · 전체메뉴 · 우측 다운로드)에서 합니다. 이 페이지는 납품 전 제거합니다.

레이아웃 · 페이지 변수

PC: inner 1220px 안에서 좌측 웹앱 화면(768px) + 우측 앱 다운로드 (space-between). 1280px 이하에서는 우측 영역을 숨기고 웹앱 화면만 가운데 정렬합니다.

<body class="app-body">
<div class="app-wrap">                 ← inner 1220 / space-between
    <div class="app-frame">            ← 웹앱 화면 768
        <header class="app-header">    ← sticky (main | sub)
        <main class="app-main">        ← 페이지 본문
        <nav class="app-tabbar">       ← sticky 하단 탭바
    </div>
    <aside class="app-side">           ← 앱 다운로드 (sticky, 1280 이하 숨김)
</div>
<div class="app-layer app-menu">       ← 전체메뉴 (프레임 위치에 맞춰 열림)

페이지 상단 변수 (inc/header.php)

페이지 변수
변수설명
$root / $app사이트 루트 / 웹앱 루트 상대경로 (app/index.php → '../' / './')
$pageId탭바 활성 (home · reserve · car · benefit · my) · 전체메뉴 활성
$headerType'main' 메뉴·로고·알림 / 'sub' 뒤로가기·타이틀(h1) / 'none'
$headerTitle · $backHrefsub 헤더 타이틀 · 뒤로가기 기본 경로
$useTabbar하단 탭바 노출 (기본 true · 예약 단계 등 하단 고정 버튼 화면은 false)
$hasNewNoti알림 아이콘 새 소식 점
$pageCss · $pageJs · $useSwiper페이지 전용 CSS/JS (app 기준 경로), 스와이퍼 사용

전체메뉴 (inc/app.data.php → $appMenu)

브랜드소개 · 프로그램 · 점포안내는 웹앱 페이지, 시공사례 · Q&A 는 'site' => true 로 공홈 연결 (↗ 아이콘).

홈 상태별 미리보기

app/index.php?state= 값으로 확인합니다.

타이포그래피

크기 · 굵기 · 행간 · 자간은 아래 클래스로만 지정합니다. 컴포넌트 CSS 에 font-size 를 쓰지 않고, 마크업에 컴포넌트 클래스 + 타이포 클래스를 함께 붙입니다. 태그(h1~h6)는 문서 구조대로 쓰고 .h1 ~ .h4 는 크기 이름입니다. (자간 모두 0)

  • .display-l32 / 600 / 125%

    내 차를 아끼는 습관

  • .display-m28 / 600 / 140%

    내 차를 아끼는 습관

  • .display-s24 / 600 / 135%

    내 차를 아끼는 습관

  • .h122 / 600 / 140%

    홍예린 고객님, 예약하신 케어가 있어요.

  • .h220 / 600 / 150%

    2000P

  • .h318 / 600 / 140%

    사용가능한 혜택

  • .h416 / 600 / 140%

    카앤쿡 동탄점

  • .body-large16 / 400 / 160%

    스팀 손세차부터 실내 클리닝까지 세심하게 관리합니다.

  • .body-medium15 / 400 / 160%

    최근 관리 이력이 없습니다.

  • .body-small14 / 400 / 160%

    묵은 오염을 집중 관리

  • .btn-l16 / 600 / 160%

    예약 상세 보기

  • .btn-m15 / 500 / 150%

    인증요청

  • .btn-s14 / 500 / 160%

    차량 관리

  • .caption-l14 / 400 / 150%

    MY CAR · M

  • .caption-m13 / 400 / 150%

    내·외부 스팀 손세차

  • .caption-s12 / 400 / 140%

    홈 · 예약 · 내 차

사용가능한 혜택

예약 상세 보기

색 · 굵기 유틸 : .static1-txt .gray6-txt .gray7-txt .gray8-txt .white-txt .red-txt / .weight400 ~ .weight700

코드 보기
<h2 class="h3">사용가능한 혜택</h2>
<a class="square-btn full main-btn btn-l" href="#none">예약 상세 보기</a>
<p class="body-small gray6-txt">색 · 굵기 유틸 : .static1-txt .gray6-txt .gray7-txt .gray8-txt .white-txt .red-txt / .weight400 ~ .weight700</p>

컬러 토큰

--color-gray5 (#A3A3A3) 이하 밝은 회색은 흰 배경 대비 부족 → 텍스트에 쓰지 않습니다. 텍스트 최소 회색은 --color-gray6.

브랜드 · 배경 · 라인

  • --color-static1
    #1F463B
  • --color-static2
    #262626
  • --color-static3
    #EAF1ED
  • --color-bg1
    #FFFFFF
  • --color-bg2
    #F5F4EF
  • --color-border
    #E5E5E5

그레이

  • --color-gray1
    #FAFAFA
  • --color-gray2
    #F5F5F5
  • --color-gray3
    #E5E5E5
  • --color-gray4
    #D4D4D4
  • --color-gray5
    #A3A3A3
  • --color-gray6
    #737373
  • --color-gray7
    #525252
  • --color-gray8
    #404040
  • --color-gray9
    #262626
  • --color-gray10
    #171717
  • --color-gray11
    #0A0A0A

상태 (컬러칩 외)

  • --color-red
    #D32F2F
  • --color-redbg
    #FFEDEE

아이콘

inc/icons.php 인라인 SVG. <?= icon('이름', 크기, '클래스') ?> · 색은 부모 color(currentColor)를 따릅니다. 추가는 $icons 배열에 path 만 넣으면 됩니다.

  • menu
  • bell
  • close
  • back
  • chevron
  • chevron-down
  • external
  • search
  • check
  • plus
  • place
  • time
  • tel
  • point
  • coupon
  • membership
  • tab-home
  • tab-reserve
  • tab-car
  • tab-benefit
  • tab-my

버튼

모양(.square-btn .ico-btn .txt-btn) + 색(.main-btn .dark-btn .sub-btn .line-btn .gray-btn) 조합. 이동은 <a>, 동작은 <button type="button">.

square-btn 크기 · 색

기본 52 (full)
코드 보기
<a class="square-btn full main-btn btn-l" href="#none">기본 52 (full)</a>
<div class="btn-list">
    <button type="button" class="square-btn medium main-btn btn-m">medium 48</button>
    <button type="button" class="square-btn medium dark-btn btn-m">dark</button>
    <button type="button" class="square-btn medium sub-btn btn-m">sub</button>
    <button type="button" class="square-btn medium line-btn btn-m">line</button>
    <button type="button" class="square-btn medium gray-btn btn-m">gray</button>
</div>
<div class="btn-list">
    <button type="button" class="square-btn small line-btn btn-s">small 40</button>
    <button type="button" class="square-btn medium main-btn btn-m" disabled>비활성</button>
</div>

버튼 2개 (같은 너비)

예약 변경
코드 보기
<div class="btn-list full">
    <button type="button" class="square-btn line-btn btn-l">예약 취소</button>
    <a class="square-btn main-btn btn-l" href="#none">예약 변경</a>
</div>

txt-btn · ico-btn

코드 보기
<a class="txt-btn btn-s" href="#none">차량 관리<span class="sr-only"> - 현대 쏘나타</span><?= icon('chevron', 16) ?></a>
<div class="btn-list">
    <button type="button" class="ico-btn" aria-label="전체 메뉴 열기"><?= icon('menu') ?></button>
    <a class="ico-btn" href="#none" aria-label="알림"><?= icon('bell') ?></a>
    <button type="button" class="ico-btn" aria-label="닫기"><?= icon('close') ?></button>
</div>

Form

모든 입력 요소에 <label for> 와 name 필수. 글자 크기 16px 미만 금지(iOS 확대). 오류는 .error + aria-invalid + aria-describedby.

custom-input · 상태

예약 안내 문자가 발송됩니다.

인증번호가 일치하지 않습니다.

코드 보기
<div class="input-item">
    <label class="caption-m input-item-label req" for="cpCarNum">차량번호<span class="sr-only">(필수)</span></label>
    <input class="custom-input body-large" type="text" id="cpCarNum" name="car_number" placeholder="예) 123가 4567" required>
</div>
<div class="input-item">
    <label class="caption-m input-item-label" for="cpPhone">휴대폰 번호</label>
    <div class="input-wrap">
        <input class="custom-input body-large" type="tel" id="cpPhone" name="phone" inputmode="numeric" autocomplete="tel" aria-describedby="cpPhoneInfo">
        <button type="button" class="square-btn dark-btn btn-m">인증요청</button>
    </div>
    <p class="info-msg caption-m" id="cpPhoneInfo">예약 안내 문자가 발송됩니다.</p>
</div>
<div class="input-item">
    <label class="caption-m input-item-label" for="cpCode">인증번호</label>
    <input class="custom-input error body-large" type="text" id="cpCode" name="auth_code" inputmode="numeric" value="1234" aria-invalid="true" aria-describedby="cpCodeError">
    <p class="error-msg caption-m" id="cpCodeError">인증번호가 일치하지 않습니다.</p>
</div>
<div class="input-item">
    <label class="caption-m input-item-label" for="cpDisabled">비활성</label>
    <input class="custom-input body-large" type="text" id="cpDisabled" name="disabled_sample" value="수정할 수 없음" disabled>
</div>

custom-select · custom-textarea

코드 보기
<div class="input-item">
    <label class="caption-m input-item-label" for="cpSize">차량 크기</label>
    <select class="custom-select body-large" id="cpSize" name="car_size">
        <option value="">선택해주세요</option>
        <option value="S">소형 (S)</option>
        <option value="M">중형 (M)</option>
        <option value="L">대형 · SUV (L)</option>
    </select>
</div>
<div class="input-item">
    <label class="caption-m input-item-label" for="cpMemo">요청사항</label>
    <textarea class="custom-textarea body-large" id="cpMemo" name="memo" maxlength="300"></textarea>
</div>

체크박스 · 라디오 · 스위치

결제 수단
코드 보기
<div class="check-list">
    <div class="check-item"><input class="custom-checkbox" type="checkbox" id="cpAgree1" name="agree_terms" checked><label class="body-medium" for="cpAgree1">이용약관 동의</label></div>
    <div class="check-item"><input class="custom-checkbox" type="checkbox" id="cpAgree2" name="agree_marketing"><label class="body-medium" for="cpAgree2">마케팅 수신 동의</label></div>
    <div class="check-item"><input class="custom-checkbox" type="checkbox" id="cpAgree3" name="agree_disabled" disabled><label class="body-medium" for="cpAgree3">비활성</label></div>
</div>
<fieldset>
    <legend class="caption-m input-item-label">결제 수단</legend>
    <div class="check-list">
        <div class="check-item"><input class="custom-radio" type="radio" id="cpPay1" name="pay" value="card" checked><label class="body-medium" for="cpPay1">카드</label></div>
        <div class="check-item"><input class="custom-radio" type="radio" id="cpPay2" name="pay" value="membership"><label class="body-medium" for="cpPay2">회원권</label></div>
        <div class="check-item"><input class="custom-radio" type="radio" id="cpPay3" name="pay" value="onsite" disabled><label class="body-medium" for="cpPay3">현장결제</label></div>
    </div>
</fieldset>
<div class="check-item"><input class="custom-switch" type="checkbox" role="switch" id="cpPush" name="push" checked><label class="body-medium" for="cpPush">알림 받기</label></div>

select-card (지점 · 코스 선택)

코스 선택
코드 보기
<fieldset>
    <legend class="sr-only">코스 선택</legend>
    <ul class="select-card-list">
        <li class="select-card">
            <input type="radio" id="cpCourse1" name="course" value="daily" checked>
            <label for="cpCourse1"><span class="h4">데일리</span><span class="body-small gray6-txt">60,000원부터</span></label>
        </li>
        <!-- 복사 -->
        <li class="select-card">
            <input type="radio" id="cpCourse2" name="course" value="signature">
            <label for="cpCourse2"><span class="h4">시그니처</span><span class="body-small gray6-txt">90,000원부터</span></label>
        </li>
        <li class="select-card">
            <input type="radio" id="cpCourse3" name="course" value="soldout" disabled>
            <label for="cpCourse3"><span class="h4">리뉴</span><span class="body-small">예약 마감</span></label>
        </li>
    </ul>
</fieldset>

뱃지 · 상태

시공중 예약확정 이용완료 BEST 예약취소 M

영업중 · 09:00 - 20:00

영업종료 · 09:00 - 20:00

코드 보기
<div class="btn-list">
    <span class="badge main caption-s">시공중</span>
    <span class="badge confirm caption-s">예약확정</span>
    <span class="badge caption-s">이용완료</span>
    <span class="badge line caption-s">BEST</span>
    <span class="badge cancel caption-s">예약취소</span>
    <span class="badge main square caption-s">M</span>
</div>
<p class="body-small"><span class="state-txt open">영업중</span> · 09:00 - 20:00</p>
<p class="body-small"><span class="state-txt close">영업종료</span> · 09:00 - 20:00</p>

탭

[data-tab] 안의 [role="tab"](.active) ↔ .tab-content(.active). 방향키 ← → · Home · End 이동.

밑줄형 .tab-list

예약중 패널

이용완료 패널

취소 패널

코드 보기
<div data-tab>
    <div class="tab-list" role="tablist" aria-label="예약 내역">
        <button type="button" class="tab active btn-m" role="tab" id="cpRvTab1" aria-selected="true" aria-controls="cpRvPanel1">예약중</button>
        <button type="button" class="tab btn-m" role="tab" id="cpRvTab2" aria-selected="false" aria-controls="cpRvPanel2" tabindex="-1">이용완료</button>
        <button type="button" class="tab btn-m" role="tab" id="cpRvTab3" aria-selected="false" aria-controls="cpRvPanel3" tabindex="-1">취소</button>
    </div>
    <div class="tab-content active" role="tabpanel" id="cpRvPanel1" aria-labelledby="cpRvTab1" tabindex="0"><p class="body-small">예약중 패널</p></div>
    <div class="tab-content" role="tabpanel" id="cpRvPanel2" aria-labelledby="cpRvTab2" tabindex="0"><p class="body-small">이용완료 패널</p></div>
    <div class="tab-content" role="tabpanel" id="cpRvPanel3" aria-labelledby="cpRvTab3" tabindex="0"><p class="body-small">취소 패널</p></div>
</div>

세그먼트 .seg-tab-list

포인트 패널

쿠폰 패널

회원권 패널

코드 보기
<div data-tab>
    <div class="seg-tab-list" role="tablist" aria-label="혜택">
        <button type="button" class="tab active btn-s" role="tab" id="cpBnTab1" aria-selected="true" aria-controls="cpBnPanel1">포인트</button>
        <button type="button" class="tab btn-s" role="tab" id="cpBnTab2" aria-selected="false" aria-controls="cpBnPanel2" tabindex="-1">쿠폰</button>
        <button type="button" class="tab btn-s" role="tab" id="cpBnTab3" aria-selected="false" aria-controls="cpBnPanel3" tabindex="-1">회원권</button>
    </div>
    <div class="tab-content active" role="tabpanel" id="cpBnPanel1" aria-labelledby="cpBnTab1" tabindex="0"><p class="body-small">포인트 패널</p></div>
    <div class="tab-content" role="tabpanel" id="cpBnPanel2" aria-labelledby="cpBnTab2" tabindex="0"><p class="body-small">쿠폰 패널</p></div>
    <div class="tab-content" role="tabpanel" id="cpBnPanel3" aria-labelledby="cpBnTab3" tabindex="0"><p class="body-small">회원권 패널</p></div>
</div>

목록

info-list (라벨 ↔ 값 양끝 정렬)

예약 날짜
코스
시그니처 코스
지점명
카앤쿡 창원점
코드 보기
<dl class="info-list body-small">
    <div><dt>예약 날짜</dt><dd><time datetime="2026-09-27T14:00">09.27 SAT 14:00</time></dd></div>
    <div><dt>코스</dt><dd>시그니처 코스</dd></div>
    <div><dt>지점명</dt><dd>카앤쿡 창원점</dd></div>
</dl>

info-list.inline (라벨 : 값)

최근 관리
· 4일전
다음 관리
약 10일 후 추천
코드 보기
<dl class="info-list inline body-small">
    <div><dt>최근 관리</dt><dd><time datetime="2026-09-12">2026.09.12</time> · 4일전</dd></div>
    <div><dt>다음 관리</dt><dd>약 10일 후 추천</dd></div>
</dl>

dot-ul · link-list

  • 예약 변경은 방문 하루 전까지 가능합니다.
  • 차량 상태에 따라 작업 시간이 달라질 수 있습니다.
코드 보기
<ul class="dot-ul caption-m">
    <li>예약 변경은 방문 하루 전까지 가능합니다.</li>
    <li>차량 상태에 따라 작업 시간이 달라질 수 있습니다.</li>
</ul>
<ul class="link-list body-large">
    <li><a href="#none">예약 내역<?= icon('chevron', 20) ?></a></li>
    <li><a href="#none">시공 이력<?= icon('chevron', 20) ?></a></li>
    <li><a href="#none">알림 설정<?= icon('chevron', 20) ?></a></li>
</ul>

내 차 카드 .mycar-card

inc/components.php → mycarCard($car, $mode, $data, $opt). $mode : reserved · working · empty · first · revisit. 차량 이미지는 배경 투명 WebP(620×280 비율)로 받습니다.

예약 있음 — 'reserved'

시공중 — 'working'

현재 작업 상태

시공 중 · 약 40분 경과

  1. 입고 완료(완료)
  2. 작업 준비(완료)
  3. 시공 중(진행 중)
  4. 작업 완료

등록된 차량 없음 — 'empty'

MY CAR

등록된 차량이 없습니다.

차량을 등록해주세요.

첫 예약 (관리 이력 없음) — 'first'

재예약 (관리 이력 있음) — 'revisit'

카드 · 배너

search-banner

코드 보기
<a class="search-banner" href="#none">
    <span class="banner-txt">
        <span class="banner-tit h4"><?= icon('search', 20) ?>내 차 맞춤관리 찾기</span>
        <span class="banner-desc caption-m">차량 사이즈별 가격을 바로 확인하세요.</span>
    </span>
    <?= icon('chevron', 20) ?>
</a>

benefit-box

코드 보기
<ul class="benefit-box shadow-box">
    <li><a href="#none"><?= icon('point', 20) ?><strong class="benefit-num h2">2000P</strong><span class="benefit-label body-small">포인트</span></a></li>
    <li><a href="#none"><?= icon('coupon', 20) ?><strong class="benefit-num h2">1장</strong><span class="benefit-label body-small">사용 가능 쿠폰</span></a></li>
    <li><a href="#none"><?= icon('membership', 20) ?><strong class="benefit-num h2">10개</strong><span class="benefit-label body-small">남은 회원권</span></a></li>
</ul>

program-card (가로 스크롤 .h-scroll)

코드 보기
<ul class="h-scroll">
    <li>
        <a class="program-card" href="#none">
            <h4 class="program-tit h4">데일리<?= icon('chevron', 18) ?></h4>
            <p class="program-desc body-small">일상 오염을 가볍게</p>
            <p class="program-sub caption-m">내·외부 스팀 손세차</p>
            <p class="program-price body-medium">60,000원부터</p>
        </a>
    </li>
    <!-- 복사 -->
    <li>
        <a class="program-card" href="#none">
            <h4 class="program-tit h4">시그니처<?= icon('chevron', 18) ?></h4>
            <p class="program-desc body-small">세차에 보호와 위생까지</p>
            <p class="program-sub caption-m">왁스·가죽·타이어 보호</p>
            <p class="program-price body-medium">90,000원부터</p>
        </a>
    </li>
</ul>

recommend-card

코드 보기
<a class="recommend-card shadow-box" href="#none">
    <div class="recommend-img"><img src="img/home-renew.webp" alt="리뉴 프로그램으로 관리한 차량" width="724" height="362" loading="lazy" decoding="async"></div>
    <div class="recommend-body">
        <h4 class="recommend-tit h3">리뉴</h4>
        <p class="recommend-desc body-small">묵은 오염을 집중 관리</p>
        <p class="recommend-price h3">280,000원부터</p>
        <?= icon('chevron', 20) ?>
    </div>
</a>

store-card

코드 보기
<ul class="store-list">
    <li>
        <a class="store-card shadow-box" href="#none">
            <span class="store-img"><img src="img/store-thumb-01.webp" alt="" aria-hidden="true" width="140" height="112" loading="lazy" decoding="async"></span>
            <div class="store-info">
                <h4 class="store-name h4">카앤쿡 동탄점</h4>
                <p class="store-addr body-small">경기도 화성시 동탄구 지산1길 25-5 1층</p>
                <p class="store-time body-small"><span class="state-txt open">영업중</span> · 09:00 - 20:00</p>
            </div>
        </a>
    </li>
</ul>

현재 작업 상태 · 진행 단계

inc/components.php → workStateBox($work, $tag). $work['step'] = 현재 단계 index (0 입고 완료 · 1 작업 준비 · 2 시공 중 · 3 작업 완료). 지난 단계 li.done, 현재 단계 li.current + aria-current="step".

work-state-box

현재 작업 상태

시공 중 · 약 40분 경과

  1. 입고 완료(완료)
  2. 작업 준비(완료)
  3. 시공 중(진행 중)
  4. 작업 완료

progress-step 단계별

  1. 입고 완료(진행 중)
  2. 작업 준비
  3. 시공 중
  4. 작업 완료
  1. 입고 완료(완료)
  2. 작업 준비(완료)
  3. 시공 중(완료)
  4. 작업 완료(진행 중)
코드 보기
<ol class="progress-step" aria-label="작업 진행 단계">
    <li class="current" aria-current="step"><span class="step-dot" aria-hidden="true"></span><span class="step-txt body-small">입고 완료<span class="sr-only">(진행 중)</span></span></li>
    <li><span class="step-dot" aria-hidden="true"></span><span class="step-txt body-small">작업 준비</span></li>
    <li><span class="step-dot" aria-hidden="true"></span><span class="step-txt body-small">시공 중</span></li>
    <li><span class="step-dot" aria-hidden="true"></span><span class="step-txt body-small">작업 완료</span></li>
</ol>
<ol class="progress-step" aria-label="작업 진행 단계">
    <li class="done"><span class="step-dot" aria-hidden="true"><?= icon('check', 16) ?></span><span class="step-txt body-small">입고 완료<span class="sr-only">(완료)</span></span></li>
    <li class="done"><span class="step-dot" aria-hidden="true"><?= icon('check', 16) ?></span><span class="step-txt body-small">작업 준비<span class="sr-only">(완료)</span></span></li>
    <li class="done"><span class="step-dot" aria-hidden="true"><?= icon('check', 16) ?></span><span class="step-txt body-small">시공 중<span class="sr-only">(완료)</span></span></li>
    <li class="current" aria-current="step"><span class="step-dot" aria-hidden="true"></span><span class="step-txt body-small">작업 완료<span class="sr-only">(진행 중)</span></span></li>
</ol>

바텀시트 .bottom-sheet

열기 [data-layer-open="시트id"] · 닫기 [data-layer-close] · ESC · 딤 클릭. 포커스 트랩 · 스크롤 잠금 · 닫으면 열었던 버튼으로 포커스 복귀. 마크업은 </main> 뒤에 둡니다.

<div class="app-layer bottom-sheet" id="cpSheet" data-layer inert>
    <div class="app-wrap">
        <div class="app-layer-area">
            <div class="app-layer-dim" data-layer-close></div>
            <div class="sheet-panel" role="dialog" aria-modal="true" aria-labelledby="cpSheetTit2">
                <div class="sheet-head">
                    <h2 class="h3" id="cpSheetTit2">차량 선택</h2>
                    <button type="button" class="ico-btn" data-layer-close aria-label="닫기"><?= icon('close') ?></button>
                </div>
                <div class="sheet-body"> … </div>
                <div class="sheet-foot"><button type="button" class="square-btn full main-btn btn-l">선택 완료</button></div>
            </div>
        </div>
    </div>
</div>

빈 상태 .empty-box

가까운 매장을 찾지 못했어요.

위치 권한을 허용하거나 전체 매장을 확인해주세요.

전체 매장 보기
코드 보기
<div class="empty-box shadow-box">
    <?= icon('place', 40) ?>
    <p class="empty-tit h4">가까운 매장을 찾지 못했어요.</p>
    <p class="empty-desc body-small">위치 권한을 허용하거나 전체 매장을 확인해주세요.</p>
    <a class="square-btn small line-btn btn-s" href="#none">전체 매장 보기</a>
</div>