Files
joke-app/apps/web/src/components/cards/PlayingCard.tsx
EJClaw 9519b09be8 M3 기반: 공용 베팅 엔진, 카드 정의, 카드·칩 화면 컴포넌트, 돈 표현 금지 검사
- packages/engine/betting: 한국식(체크·삥·콜·따당·쿼터·하프·풀·다이, 라운드당 1회·첫 행동에서만 레이즈),
  노리밋·팟리밋(최소 레이즈, 불완전 올인 레이즈는 재오픈 안 함, BB 옵션), 앤티·블라인드, 올인,
  받지 않은 베팅 반환, 사이드 팟 분할, 동점 분배(홀수 칩 순서), 시간 초과 자동 행동
- 테스트 24개: 포커·섯다 계획서 계산 예시, 행동별 상태 전이, 사이드 팟 예시, 무작위 베팅 1,500판
  (칩 총량 보존·라운드 종료·라운드 끝 금액 일치·팟 합계=판 전체)
- packages/engine/cards: 트럼프 52+조커, 화투 48(+보너스)·섯다 20장 정의와 테스트
- 웹: 직접 그린 SVG 트럼프·화투 카드, 칩 표시("1,250 칩"), 놀이용 칩 안내문
- apps/web/src/no-money-wording.test.ts: 베팅 게임 화면에 통화 기호·"원"·판돈·충전·환전 등 금지

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 05:45:13 +09:00

103 lines
4.1 KiB
TypeScript

/** Trump card drawn in SVG (no image assets). Big corner index for readability on PC and phones. */
import type { CSSProperties } from 'react';
const SUIT_SYMBOL: Record<string, string> = { S: '♠', H: '♥', D: '◆', C: '♣' };
const SUIT_KO: Record<string, string> = { S: '스페이드', H: '하트', D: '다이아', C: '클로버' };
const RANK_LABEL: Record<string, string> = { T: '10', J: 'J', Q: 'Q', K: 'K', A: 'A' };
const FACE_KO: Record<string, string> = { J: '잭', Q: '퀸', K: '킹', A: '에이스' };
export type CardSize = 'sm' | 'md' | 'lg';
const WIDTH: Record<CardSize, number> = { sm: 44, md: 64, lg: 88 };
export interface PlayingCardProps {
/** e.g. "AS", "TD", "X1" (joker). null = face down. */
card: string | null;
size?: CardSize;
selected?: boolean;
dimmed?: boolean;
highlight?: boolean;
onClick?: () => void;
title?: string;
style?: CSSProperties;
/** Colour-blind friendly: 4-colour suits. */
fourColor?: boolean;
}
export function cardAria(card: string | null): string {
if (!card) return '뒤집힌 카드';
if (card.startsWith('X')) return '조커';
const r = card[0]!;
return `${SUIT_KO[card[1]!]} ${FACE_KO[r] ?? RANK_LABEL[r] ?? r}`;
}
export function PlayingCard({ card, size = 'md', selected, dimmed, highlight, onClick, title, style, fourColor }: PlayingCardProps) {
const w = WIDTH[size];
const h = Math.round(w * 1.4);
const lift = selected ? -Math.round(w * 0.22) : 0;
const common: CSSProperties = {
width: w,
height: h,
flex: 'none',
transform: `translateY(${lift}px)`,
transition: 'transform 0.12s ease',
cursor: onClick ? 'pointer' : 'default',
opacity: dimmed ? 0.45 : 1,
filter: highlight ? 'drop-shadow(0 0 6px #f59e0b)' : 'drop-shadow(0 1px 2px rgba(0,0,0,0.25))',
...style,
};
const Tag = onClick ? 'button' : 'span';
const btnReset: CSSProperties = onClick ? { padding: 0, border: 0, background: 'none' } : {};
if (!card) {
return (
<Tag style={{ ...common, ...btnReset }} onClick={onClick} aria-label={title ?? '뒤집힌 카드'} title={title}>
<svg viewBox="0 0 100 140" width={w} height={h} aria-hidden>
<rect x="1" y="1" width="98" height="138" rx="9" fill="#1e3a8a" stroke="#fff" strokeWidth="3" />
<rect x="9" y="9" width="82" height="122" rx="6" fill="none" stroke="#93c5fd" strokeWidth="2" />
<path d="M50 30 L70 70 L50 110 L30 70 Z" fill="#3b82f6" opacity="0.7" />
</svg>
</Tag>
);
}
const joker = card.startsWith('X');
const suit = card[1]!;
const rank = card[0]!;
const color = joker
? card === 'X1' ? '#111827' : '#dc2626'
: fourColor
? { S: '#111827', H: '#dc2626', D: '#2563eb', C: '#15803d' }[suit]!
: suit === 'H' || suit === 'D'
? '#dc2626'
: '#111827';
const label = joker ? '★' : (RANK_LABEL[rank] ?? rank);
const sym = joker ? '' : SUIT_SYMBOL[suit];
return (
<Tag style={{ ...common, ...btnReset }} onClick={onClick} aria-label={title ?? cardAria(card)} aria-pressed={onClick ? !!selected : undefined} title={title}>
<svg viewBox="0 0 100 140" width={w} height={h} aria-hidden>
<rect x="1" y="1" width="98" height="138" rx="9" fill="#fffef9" stroke={selected ? '#2563eb' : '#9ca3af'} strokeWidth={selected ? 5 : 2} />
<text x="9" y="33" fontSize="30" fontWeight="800" fill={color} fontFamily="Pretendard Variable, sans-serif">
{label}
</text>
<text x="11" y="60" fontSize="26" fill={color}>
{sym}
</text>
{joker ? (
<text x="50" y="98" fontSize="22" fontWeight="800" textAnchor="middle" fill={color}>
JOKER
</text>
) : 'JQK'.includes(rank) ? (
<>
<rect x="30" y="44" width="56" height="78" rx="6" fill={color} opacity="0.1" />
<text x="58" y="100" fontSize="44" fontWeight="800" textAnchor="middle" fill={color}>
{rank}
</text>
</>
) : (
<text x="60" y="108" fontSize="54" textAnchor="middle" fill={color}>
{sym}
</text>
)}
</svg>
</Tag>
);
}