02스탬프 적립 카드
주문 한 번에 도장 한 칸, 열 칸을 다 채우면 쿠폰이 발급되는 적립 카드입니다. 도장은 크기 2배에서 1로 내리찍히며 살짝 기울고, 진행률은 aria-valuenow에 같이 반영됩니다.
쓰임카페·멤버십 적립 카드, 주문 완료 화면의 도장판
transform-originscalearia-valuenow

<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<symbol id="seal" viewBox="0 0 256 256" fill="currentColor"><path d="M232,128c0,12.51-17.82,21.95-22.68,33.69-4.68,11.32,1.42,30.65-7.78,39.85s-28.53,3.1-39.85,7.78C150,214.18,140.5,232,128,232s-22-17.82-33.69-22.68c-11.32-4.68-30.65,1.42-39.85-7.78s-3.1-28.53-7.78-39.85C41.82,150,24,140.5,24,128s17.82-22,22.68-33.69C51.36,83,45.26,63.66,54.46,54.46S83,51.36,94.31,46.68C106.05,41.82,115.5,24,128,24S150,41.82,161.69,46.68c11.32,4.68,30.65-1.42,39.85,7.78s3.1,28.53,7.78,39.85C214.18,106.05,232,115.5,232,128Z" opacity="0.2"/><path d="M225.86,102.82c-3.77-3.94-7.67-8-9.14-11.57-1.36-3.27-1.44-8.69-1.52-13.94-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52-3.56-1.47-7.63-5.37-11.57-9.14C146.28,23.51,138.44,16,128,16s-18.27,7.51-25.18,14.14c-3.94,3.77-8,7.67-11.57,9.14C88,40.64,82.56,40.72,77.31,40.8c-9.76.15-20.82.31-28.51,8S41,67.55,40.8,77.31c-.08,5.25-.16,10.67-1.52,13.94-1.47,3.56-5.37,7.63-9.14,11.57C23.51,109.72,16,117.56,16,128s7.51,18.27,14.14,25.18c3.77,3.94,7.67,8,9.14,11.57,1.36,3.27,1.44,8.69,1.52,13.94.15,9.76.31,20.82,8,28.51s18.75,7.85,28.51,8c5.25.08,10.67.16,13.94,1.52,3.56,1.47,7.63,5.37,11.57,9.14C109.72,232.49,117.56,240,128,240s18.27-7.51,25.18-14.14c3.94-3.77,8-7.67,11.57-9.14,3.27-1.36,8.69-1.44,13.94-1.52,9.76-.15,20.82-.31,28.51-8s7.85-18.75,8-28.51c.08-5.25.16-10.67,1.52-13.94,1.47-3.56,5.37-7.63,9.14-11.57C232.49,146.28,240,138.44,240,128S232.49,109.73,225.86,102.82Zm-11.55,39.29c-4.79,5-9.75,10.17-12.38,16.52-2.52,6.1-2.63,13.07-2.73,19.82-.1,7-.21,14.33-3.32,17.43s-10.39,3.22-17.43,3.32c-6.75.1-13.72.21-19.82,2.73-6.35,2.63-11.52,7.59-16.52,12.38S132,224,128,224s-9.15-4.92-14.11-9.69-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82-2.63-6.35-7.59-11.52-12.38-16.52S32,132,32,128s4.92-9.15,9.69-14.11,9.75-10.17,12.38-16.52c2.52-6.1,2.63-13.07,2.73-19.82.1-7,.21-14.33,3.32-17.43S70.51,56.9,77.55,56.8c6.75-.1,13.72-.21,19.82-2.73,6.35-2.63,11.52-7.59,16.52-12.38S124,32,128,32s9.15,4.92,14.11,9.69,10.17,9.75,16.52,12.38c6.1,2.52,13.07,2.63,19.82,2.73,7,.1,14.33.21,17.43,3.32s3.22,10.39,3.32,17.43c.1,6.75.21,13.72,2.73,19.82,2.63,6.35,7.59,11.52,12.38,16.52S224,124,224,128,219.08,137.15,214.31,142.11ZM173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34Z"/></symbol>
<symbol id="coffee" viewBox="0 0 256 256" fill="currentColor"><path d="M208,88v48a88,88,0,0,1-51.3,80H83.3A88,88,0,0,1,32,136V88Z" opacity="0.2"/><path d="M80,56V24a8,8,0,0,1,16,0V56a8,8,0,0,1-16,0Zm40,8a8,8,0,0,0,8-8V24a8,8,0,0,0-16,0V56A8,8,0,0,0,120,64Zm32,0a8,8,0,0,0,8-8V24a8,8,0,0,0-16,0V56A8,8,0,0,0,152,64Zm96,56v8a40,40,0,0,1-37.51,39.91,96.59,96.59,0,0,1-27,40.09H208a8,8,0,0,1,0,16H32a8,8,0,0,1,0-16H56.54A96.3,96.3,0,0,1,24,136V88a8,8,0,0,1,8-8H208A40,40,0,0,1,248,120ZM200,96H40v40a80.27,80.27,0,0,0,45.12,72h69.76A80.27,80.27,0,0,0,200,136Zm32,24a24,24,0,0,0-16-22.62V136a95.78,95.78,0,0,1-1.2,15A24,24,0,0,0,232,128Z"/></symbol>
</defs>
</svg>
<main class="stage">
<div class="sp is-demo" id="sp">
<div class="sp__card">
<div class="sp__head">
<p class="sp__title"><svg class="sp__ico" aria-hidden="true"><use href="#coffee"/></svg><span>커피 적립 카드</span></p>
<p class="sp__count"><span>적립</span> <b id="countNum">7/10</b></p>
</div>
<div class="sp__grid" role="progressbar" aria-label="적립 도장 진행" aria-valuemin="0" aria-valuemax="10" aria-valuenow="7" id="bar">
<span class="sp__slot"><span class="sp__stamp sp__stamp--in"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in sp__stamp--idle"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in sp__stamp--idle"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
<span class="sp__slot"><span class="sp__stamp sp__stamp--in sp__stamp--idle"><svg aria-hidden="true"><use href="#seal"/></svg></span></span>
</div>
<div class="sp__foot">
<button type="button" class="sp__btn" id="stampBtn">도장 찍기</button>
<p class="sp__issued" id="issued" aria-live="polite"><span class="sp__issuedChip">쿠폰 발급 완료</span></p>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 스탬프 적립 카드 — 도장은 scale 2→1 내리찍기 + 살짝 기울임. 진행률은 aria-valuenow 에 반영.
// 격자 미리보기(.is-demo)는 열 도장이 순서대로 찍히고 마지막에 쿠폰 발급 배지가 뜨는 2초 루프.
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-yellow);
.sp { display: flex; justify-content: center; width: 100%; }
.sp__card {
width: min(100%, 330px); background: $stage-paper; border-radius: $r-card;
box-shadow: $shadow-raised; padding: $sp-3 $sp-4; overflow: hidden;
}
.sp__head { display: flex; align-items: center; justify-content: space-between; gap: $sp-2; }
.sp__title { display: flex; align-items: center; gap: $sp-1; margin: 0; font-weight: 800; font-size: clamp(11px, 3.2vw, 13px); color: $subject-ink; }
.sp__ico { width: 16px; height: 16px; color: $subject-blue; }
.sp__count { margin: 0; font-weight: 700; font-size: clamp(10px, 2.8vw, 11px); color: rgba(23,20,26,.72); }
.sp__grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: $sp-2; margin-top: $sp-2; }
.sp__slot {
display: flex; align-items: center; justify-content: center; aspect-ratio: 1;
border: 2px dashed rgba(23,20,26,.18); border-radius: 50%; min-width: 0;
}
.sp__stamp { display: flex; width: 88%; height: 88%; color: $subject-blue; }
.sp__stamp svg { width: 100%; height: 100%; }
.sp__stamp--in { transform: rotate(-8deg); }
.sp__slot:nth-child(3n) .sp__stamp--in { transform: rotate(5deg); }
.sp__slot:nth-child(4n) .sp__stamp--in { transform: rotate(-3deg); }
.sp__stamp--new { animation: spStampIn 300ms $easing both; transform-origin: 50% 50%; }
@keyframes spStampIn {
0% { transform: scale(1.6) rotate(-14deg); }
70% { transform: scale(.94) rotate(-6deg); }
100% { transform: scale(1) rotate(-8deg); }
}
.sp__foot { position: relative; display: flex; justify-content: center; margin-top: $sp-3; min-height: 32px; }
.sp__btn {
border: 0; cursor: pointer; background: $subject-ink; color: $stage-yellow;
border-radius: $r-pill; font-weight: 800; font-size: clamp(11px, 3vw, 12px);
padding: $sp-2 $sp-5; font-family: inherit; line-height: 1.2;
transition: transform 140ms cubic-bezier(.2,.8,.2,1);
}
.sp__btn:active { transform: scale(.94); }
.sp__btn:focus-visible { outline: 3px solid $subject-blue; outline-offset: 2px; }
.sp__btn:disabled { cursor: default; }
.sp__issued { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; margin: 0; visibility: hidden; }
.sp__issued.is-on { visibility: visible; }
.sp__issuedChip {
background: #a86253; color: $subject-cream; border-radius: $r-pill;
font-weight: 800; font-size: clamp(11px, 3vw, 12px); padding: $sp-2 $sp-4;
animation: spBadgeIn 300ms $easing both;
}
@keyframes spBadgeIn { 0% { transform: scale(.4); } 70% { transform: scale(1.08); } 100% { transform: scale(1); } }
// ── 격자 미리보기 루프: 정적 도장 7개는 그대로 두고, 남은 3칸 + 리셋 컷으로 2초를 채운다.
// is-demo 동안은 정적 7개를 감추고 0→10 전체 시퀀스를 보여준다 (delay 는 낱개 속성 — 단축 리셋 함정).
.sp.is-demo .sp__stamp--in { visibility: hidden; }
.sp.is-demo .sp__slot:nth-child(n) .sp__stamp--in { animation: spLoop 2s ease-out infinite; animation-delay: calc(var(--k, 0) * 120ms); animation-fill-mode: backwards; }
.sp.is-demo .sp__slot:nth-child(1) .sp__stamp--in { --k: 0; }
.sp.is-demo .sp__slot:nth-child(2) .sp__stamp--in { --k: 1; }
.sp.is-demo .sp__slot:nth-child(3) .sp__stamp--in { --k: 2; }
.sp.is-demo .sp__slot:nth-child(4) .sp__stamp--in { --k: 3; }
.sp.is-demo .sp__slot:nth-child(5) .sp__stamp--in { --k: 4; }
.sp.is-demo .sp__slot:nth-child(6) .sp__stamp--in { --k: 5; }
.sp.is-demo .sp__slot:nth-child(7) .sp__stamp--in { --k: 6; }
.sp.is-demo .sp__slot:nth-child(8) .sp__stamp--in { --k: 7; }
.sp.is-demo .sp__slot:nth-child(9) .sp__stamp--in { --k: 8; }
.sp.is-demo .sp__slot:nth-child(10) .sp__stamp--in { --k: 9; }
// 슬롯 8~10 의 도장은 정적 상태에서는 비어 있는 칸으로 보이고 루프에서만 찍힌다
.sp:not(.is-demo) .sp__stamp--idle { visibility: hidden; }
@keyframes spLoop {
0% { visibility: hidden; transform: scale(1.6) rotate(-14deg); }
1.5% { visibility: visible; }
8% { transform: scale(.94) rotate(-6deg); }
12%, 96% { visibility: visible; transform: scale(1) rotate(-8deg); }
96.5%, 100% { visibility: hidden; transform: scale(1) rotate(-8deg); }
}
.sp.is-demo .sp__btn { animation: spBtnHide 2s steps(1, end) infinite; }
@keyframes spBtnHide { 0%, 72% { visibility: visible; } 72.5%, 96% { visibility: hidden; } 96.5%, 100% { visibility: visible; } }
.sp.is-demo .sp__issued { visibility: visible; }
.sp.is-demo .sp__issuedChip { animation: spBadgeLoop 2s ease-out infinite; }
@keyframes spBadgeLoop {
0%, 71% { transform: scale(0); }
74% { transform: scale(1.1); }
77%, 95% { transform: scale(1); }
97%, 100% { transform: scale(0); }
}
@media (max-width: 340px) {
.sp__card { padding: $sp-2 $sp-3; }
.sp__grid { gap: $sp-1; margin-top: $sp-1; }
.sp__foot { margin-top: $sp-1; min-height: 24px; }
.sp__slot { border-width: 1px; }
.sp__btn { padding: $sp-1 $sp-4; }
.sp__issuedChip { padding: $sp-1 $sp-3; }
}
@media (prefers-reduced-motion: reduce) {
.sp.is-demo .sp__slot .sp__stamp--in,
.sp.is-demo .sp__btn,
.sp.is-demo .sp__issuedChip,
.sp__stamp--new,
.sp__issuedChip { animation: none !important; }
.sp.is-demo .sp__stamp--in { visibility: visible !important; }
.sp__btn { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sp {
display: flex;
justify-content: center;
width: 100%;
}
.sp__card {
width: min(100%, 330px);
background: #ffffff;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 12px 16px;
overflow: hidden;
}
.sp__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.sp__title {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: 800;
font-size: clamp(11px, 3.2vw, 13px);
color: #17141a;
}
.sp__ico {
width: 16px;
height: 16px;
color: #2f6df6;
}
.sp__count {
margin: 0;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 11px);
color: rgba(23, 20, 26, 0.72);
}
.sp__grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
margin-top: 8px;
}
.sp__slot {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
border: 2px dashed rgba(23, 20, 26, 0.18);
border-radius: 50%;
min-width: 0;
}
.sp__stamp {
display: flex;
width: 88%;
height: 88%;
color: #2f6df6;
}
.sp__stamp svg {
width: 100%;
height: 100%;
}
.sp__stamp--in {
transform: rotate(-8deg);
}
.sp__slot:nth-child(3n) .sp__stamp--in {
transform: rotate(5deg);
}
.sp__slot:nth-child(4n) .sp__stamp--in {
transform: rotate(-3deg);
}
.sp__stamp--new {
animation: spStampIn 300ms cubic-bezier(0.2, 1.4, 0.4, 1) both;
transform-origin: 50% 50%;
}
@keyframes spStampIn {
0% {
transform: scale(1.6) rotate(-14deg);
}
70% {
transform: scale(0.94) rotate(-6deg);
}
100% {
transform: scale(1) rotate(-8deg);
}
}
.sp__foot {
position: relative;
display: flex;
justify-content: center;
margin-top: 12px;
min-height: 32px;
}
.sp__btn {
border: 0;
cursor: pointer;
background: #17141a;
color: #ffd23f;
border-radius: 999px;
font-weight: 800;
font-size: clamp(11px, 3vw, 12px);
padding: 8px 20px;
font-family: inherit;
line-height: 1.2;
transition: transform 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sp__btn:active {
transform: scale(0.94);
}
.sp__btn:focus-visible {
outline: 3px solid #2f6df6;
outline-offset: 2px;
}
.sp__btn:disabled {
cursor: default;
}
.sp__issued {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
visibility: hidden;
}
.sp__issued.is-on {
visibility: visible;
}
.sp__issuedChip {
background: #a86253;
color: #fff7e6;
border-radius: 999px;
font-weight: 800;
font-size: clamp(11px, 3vw, 12px);
padding: 8px 16px;
animation: spBadgeIn 300ms cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
@keyframes spBadgeIn {
0% {
transform: scale(0.4);
}
70% {
transform: scale(1.08);
}
100% {
transform: scale(1);
}
}
.sp.is-demo .sp__stamp--in {
visibility: hidden;
}
.sp.is-demo .sp__slot:nth-child(n) .sp__stamp--in {
animation: spLoop 2s ease-out infinite;
animation-delay: calc(var(--k, 0) * 120ms);
animation-fill-mode: backwards;
}
.sp.is-demo .sp__slot:nth-child(1) .sp__stamp--in {
--k: 0;
}
.sp.is-demo .sp__slot:nth-child(2) .sp__stamp--in {
--k: 1;
}
.sp.is-demo .sp__slot:nth-child(3) .sp__stamp--in {
--k: 2;
}
.sp.is-demo .sp__slot:nth-child(4) .sp__stamp--in {
--k: 3;
}
.sp.is-demo .sp__slot:nth-child(5) .sp__stamp--in {
--k: 4;
}
.sp.is-demo .sp__slot:nth-child(6) .sp__stamp--in {
--k: 5;
}
.sp.is-demo .sp__slot:nth-child(7) .sp__stamp--in {
--k: 6;
}
.sp.is-demo .sp__slot:nth-child(8) .sp__stamp--in {
--k: 7;
}
.sp.is-demo .sp__slot:nth-child(9) .sp__stamp--in {
--k: 8;
}
.sp.is-demo .sp__slot:nth-child(10) .sp__stamp--in {
--k: 9;
}
.sp:not(.is-demo) .sp__stamp--idle {
visibility: hidden;
}
@keyframes spLoop {
0% {
visibility: hidden;
transform: scale(1.6) rotate(-14deg);
}
1.5% {
visibility: visible;
}
8% {
transform: scale(0.94) rotate(-6deg);
}
12%, 96% {
visibility: visible;
transform: scale(1) rotate(-8deg);
}
96.5%, 100% {
visibility: hidden;
transform: scale(1) rotate(-8deg);
}
}
.sp.is-demo .sp__btn {
animation: spBtnHide 2s steps(1, end) infinite;
}
@keyframes spBtnHide {
0%, 72% {
visibility: visible;
}
72.5%, 96% {
visibility: hidden;
}
96.5%, 100% {
visibility: visible;
}
}
.sp.is-demo .sp__issued {
visibility: visible;
}
.sp.is-demo .sp__issuedChip {
animation: spBadgeLoop 2s ease-out infinite;
}
@keyframes spBadgeLoop {
0%, 71% {
transform: scale(0);
}
74% {
transform: scale(1.1);
}
77%, 95% {
transform: scale(1);
}
97%, 100% {
transform: scale(0);
}
}
@media (max-width: 340px) {
.sp__card {
padding: 8px 12px;
}
.sp__grid {
gap: 4px;
margin-top: 4px;
}
.sp__foot {
margin-top: 4px;
min-height: 24px;
}
.sp__slot {
border-width: 1px;
}
.sp__btn {
padding: 4px 16px;
}
.sp__issuedChip {
padding: 4px 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.sp.is-demo .sp__slot .sp__stamp--in,
.sp.is-demo .sp__btn,
.sp.is-demo .sp__issuedChip,
.sp__stamp--new,
.sp__issuedChip {
animation: none !important;
}
.sp.is-demo .sp__stamp--in {
visibility: visible !important;
}
.sp__btn {
transition: none !important;
}
}var sp = document.getElementById('sp');
var slots = Array.prototype.slice.call(document.querySelectorAll('.sp__slot'));
var stampBtn = document.getElementById('stampBtn');
var bar = document.getElementById('bar');
var issued = document.getElementById('issued');
var countNum = document.getElementById('countNum');
var count = 7;
function render() {
bar.setAttribute('aria-valuenow', String(count));
countNum.textContent = count + '/10';
var full = count >= 10;
issued.classList.toggle('is-on', full);
stampBtn.disabled = full;
}
stampBtn.addEventListener('click', function () {
sp.classList.remove('is-demo');
if (count >= 10) { return; }
var target = slots[count].querySelector('.sp__stamp');
target.classList.remove('sp__stamp--idle');
target.classList.add('sp__stamp--new');
count = count + 1;
render();
});
render();// StampCard — vanilla 판(run/283/demo/02-coupon-stamp-card/index.html) 의 React 포트.
// 도장은 scale 1.6→1 내리찍기 + 살짝 기울임. 진행률은 role="progressbar" 의 aria-valuenow 에 그대로 실린다.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./StampCard.module.scss";
export type StampCardProps = {
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** 칸 열 개 — vanilla 의 열 개 .sp__slot 과 같은 수 */
const SLOTS: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
/** 처음 찍혀 있는 도장 수. 이보다 뒤 칸은 이번에 찍은 것이라 내리찍기 애니메이션이 붙는다 */
const START = 7;
const GOAL = 10;
export default function StampCard({
demo = true,
duration = 2000,
easing = "cubic-bezier(.2, 1.4, .4, 1)",
color = "#2f6df6",
}: StampCardProps) {
const [live, setLive] = useState(demo);
const [count, setCount] = useState(START);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const full = count >= GOAL;
// vanilla 의 click 핸들러와 같은 순서: 미리보기 루프를 떼고 다음 칸에 도장을 찍는다
function applyStamp() {
setLive(false);
if (count >= GOAL) {
return;
}
setCount(count + 1);
}
const cls = [styles.root, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<main className={styles.stage} style={vars}>
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true">
<defs>
<symbol id="seal" viewBox="0 0 256 256" fill="currentColor">
<path d="M232,128c0,12.51-17.82,21.95-22.68,33.69-4.68,11.32,1.42,30.65-7.78,39.85s-28.53,3.1-39.85,7.78C150,214.18,140.5,232,128,232s-22-17.82-33.69-22.68c-11.32-4.68-30.65,1.42-39.85-7.78s-3.1-28.53-7.78-39.85C41.82,150,24,140.5,24,128s17.82-22,22.68-33.69C51.36,83,45.26,63.66,54.46,54.46S83,51.36,94.31,46.68C106.05,41.82,115.5,24,128,24S150,41.82,161.69,46.68c11.32,4.68,30.65-1.42,39.85,7.78s3.1,28.53,7.78,39.85C214.18,106.05,232,115.5,232,128Z" opacity="0.2" />
<path d="M225.86,102.82c-3.77-3.94-7.67-8-9.14-11.57-1.36-3.27-1.44-8.69-1.52-13.94-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52-3.56-1.47-7.63-5.37-11.57-9.14C146.28,23.51,138.44,16,128,16s-18.27,7.51-25.18,14.14c-3.94,3.77-8,7.67-11.57,9.14C88,40.64,82.56,40.72,77.31,40.8c-9.76.15-20.82.31-28.51,8S41,67.55,40.8,77.31c-.08,5.25-.16,10.67-1.52,13.94-1.47,3.56-5.37,7.63-9.14,11.57C23.51,109.72,16,117.56,16,128s7.51,18.27,14.14,25.18c3.77,3.94,7.67,8,9.14,11.57,1.36,3.27,1.44,8.69,1.52,13.94.15,9.76.31,20.82,8,28.51s18.75,7.85,28.51,8c5.25.08,10.67.16,13.94,1.52,3.56,1.47,7.63,5.37,11.57,9.14C109.72,232.49,117.56,240,128,240s18.27-7.51,25.18-14.14c3.94-3.77,8-7.67,11.57-9.14,3.27-1.36,8.69-1.44,13.94-1.52,9.76-.15,20.82-.31,28.51-8s7.85-18.75,8-28.51c.08-5.25.16-10.67,1.52-13.94,1.47-3.56,5.37-7.63,9.14-11.57C232.49,146.28,240,138.44,240,128S232.49,109.73,225.86,102.82Zm-11.55,39.29c-4.79,5-9.75,10.17-12.38,16.52-2.52,6.1-2.63,13.07-2.73,19.82-.1,7-.21,14.33-3.32,17.43s-10.39,3.22-17.43,3.32c-6.75.1-13.72.21-19.82,2.73-6.35,2.63-11.52,7.59-16.52,12.38S132,224,128,224s-9.15-4.92-14.11-9.69-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82-2.63-6.35-7.59-11.52-12.38-16.52S32,132,32,128s4.92-9.15,9.69-14.11,9.75-10.17,12.38-16.52c2.52-6.1,2.63-13.07,2.73-19.82.1-7,.21-14.33,3.32-17.43S70.51,56.9,77.55,56.8c6.75-.1,13.72-.21,19.82-2.73,6.35-2.63,11.52-7.59,16.52-12.38S124,32,128,32s9.15,4.92,14.11,9.69,10.17,9.75,16.52,12.38c6.1,2.52,13.07,2.63,19.82,2.73,7,.1,14.33.21,17.43,3.32s3.22,10.39,3.32,17.43c.1,6.75.21,13.72,2.73,19.82,2.63,6.35,7.59,11.52,12.38,16.52S224,124,224,128,219.08,137.15,214.31,142.11ZM173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34Z" />
</symbol>
<symbol id="coffee" viewBox="0 0 256 256" fill="currentColor">
<path d="M208,88v48a88,88,0,0,1-51.3,80H83.3A88,88,0,0,1,32,136V88Z" opacity="0.2" />
<path d="M80,56V24a8,8,0,0,1,16,0V56a8,8,0,0,1-16,0Zm40,8a8,8,0,0,0,8-8V24a8,8,0,0,0-16,0V56A8,8,0,0,0,120,64Zm32,0a8,8,0,0,0,8-8V24a8,8,0,0,0-16,0V56A8,8,0,0,0,152,64Zm96,56v8a40,40,0,0,1-37.51,39.91,96.59,96.59,0,0,1-27,40.09H208a8,8,0,0,1,0,16H32a8,8,0,0,1,0-16H56.54A96.3,96.3,0,0,1,24,136V88a8,8,0,0,1,8-8H208A40,40,0,0,1,248,120ZM200,96H40v40a80.27,80.27,0,0,0,45.12,72h69.76A80.27,80.27,0,0,0,200,136Zm32,24a24,24,0,0,0-16-22.62V136a95.78,95.78,0,0,1-1.2,15A24,24,0,0,0,232,128Z" />
</symbol>
</defs>
</svg>
<div className={cls}>
<div className={styles.card}>
<div className={styles.head}>
<p className={styles.title}>
<svg className={styles.ico} aria-hidden="true"><use href="#coffee" /></svg>
<span>커피 적립 카드</span>
</p>
<p className={styles.count}><span>적립</span> <b>{count}/{GOAL}</b></p>
</div>
<div
className={styles.grid}
role="progressbar"
aria-label="적립 도장 진행"
aria-valuemin={0}
aria-valuemax={GOAL}
aria-valuenow={count}
>
{SLOTS.map(function (i: number) {
const idle = i >= count;
const fresh = i >= START && i < count;
const stampCls = [
styles.stamp,
styles.stampIn,
idle ? styles.stampIdle : "",
fresh ? styles.stampNew : "",
].filter(Boolean).join(" ");
return (
<span className={styles.slot} key={i}>
<span className={stampCls}><svg aria-hidden="true"><use href="#seal" /></svg></span>
</span>
);
})}
</div>
<div className={styles.foot}>
<button type="button" className={styles.btn} onClick={applyStamp} disabled={full}>도장 찍기</button>
<p className={`${styles.issued} ${full ? styles.issuedOn : ""}`} aria-live="polite">
<span className={styles.issuedChip}>쿠폰 발급 완료</span>
</p>
</div>
</div>
</div>
</main>
);
}02 스탬프 적립 카드 — 측정 누적면적 19.372% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 쿠폰 디자인 9종 — 도장부터 돌림판까지 복붙 코드
이 데모가 속한 글
쿠폰 디자인 9종 — 도장부터 돌림판까지 복붙 코드쿠폰 디자인은 할인을 받고 싶어지게 쿠폰 카드의 생김새와 움직임을 짜는 일입니다. 여기 아홉 가지는 노치 티켓·도장 적립판·돌림판·스크래치·바코드처럼 가게에서 실제로 쓰는 쿠폰 부품을 CSS 와 약간의 JS 로 만들 이 글의 데모 전부 보기


