- LoadingSpinner 컴포넌트 5가지 변형 구현 - LoadingSpinner (인라인/버튼용) - ContentLoadingSpinner (상세/수정 페이지) - PageLoadingSpinner (페이지 전환) - TableLoadingSpinner (테이블/리스트) - ButtonSpinner (버튼 내부) - 18개+ 페이지 로딩 UI 표준화 - HR 페이지 (사원, 휴가, 부서, 급여, 근태) - 영업 페이지 (견적, 거래처) - 게시판, 팝업관리, 품목기준정보 - API 키 보안 개선 (NEXT_PUBLIC_API_KEY → API_KEY) - Textarea 다크모드 스타일 개선 - DropdownField Radix UI Select 버그 수정 (key prop) - 프로젝트 헬스 개선 계획서 문서화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
184 lines
4.6 KiB
TypeScript
184 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { ReactNode } from "react";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
/**
|
|
* 목록 모바일 카드 컴포넌트
|
|
*
|
|
* 모바일 환경에서 사용하는 공통 목록 카드 컴포넌트입니다.
|
|
* 체크박스, 헤더, 뱃지, 정보 그리드, 액션 버튼을 포함합니다.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <ListMobileCard
|
|
* id="item-1"
|
|
* isSelected={false}
|
|
* onToggleSelection={() => handleToggle("item-1")}
|
|
* onCardClick={() => handleView("item-1")}
|
|
* headerBadges={[
|
|
* <Badge key="num">#{1}</Badge>,
|
|
* <Badge key="code">Q2024-001</Badge>
|
|
* ]}
|
|
* title="ABC건설"
|
|
* statusBadge={<StatusBadge variant="success" label="완료" />}
|
|
* infoGrid={
|
|
* <div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
* <InfoField label="현장명" value="강남 현장" />
|
|
* <InfoField label="담당자" value="김철수" />
|
|
* </div>
|
|
* }
|
|
* actions={isSelected && <ActionButtonGroup actions={actions} isMobile />}
|
|
* />
|
|
* ```
|
|
*/
|
|
|
|
export interface ListMobileCardProps {
|
|
/** 아이템 고유 ID */
|
|
id: string;
|
|
|
|
/** 선택 상태 */
|
|
isSelected: boolean;
|
|
|
|
/** 체크박스 토글 핸들러 */
|
|
onToggleSelection: () => void;
|
|
|
|
/** 카드 클릭 핸들러 */
|
|
onCardClick?: () => void;
|
|
|
|
/** 카드 클릭 핸들러 (onCardClick 별칭) */
|
|
onClick?: () => void;
|
|
|
|
/** 체크박스 표시 여부 */
|
|
showCheckbox?: boolean;
|
|
|
|
/** 헤더 영역 뱃지들 (번호, 코드 등) */
|
|
headerBadges?: ReactNode;
|
|
|
|
/** 카드 제목 (주요 정보) */
|
|
title: string | ReactNode;
|
|
|
|
/** 상태 뱃지 (우측 상단) */
|
|
statusBadge?: ReactNode;
|
|
|
|
/** 정보 그리드 영역 */
|
|
infoGrid: ReactNode;
|
|
|
|
/** 액션 버튼 영역 */
|
|
actions?: ReactNode;
|
|
|
|
/** 추가 className */
|
|
className?: string;
|
|
|
|
/** 카드 상단 추가 콘텐츠 */
|
|
topContent?: ReactNode;
|
|
|
|
/** 카드 하단 추가 콘텐츠 */
|
|
bottomContent?: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* 정보 필드 컴포넌트
|
|
*
|
|
* 카드 내부의 레이블-값 쌍을 표시합니다.
|
|
*/
|
|
export interface InfoFieldProps {
|
|
label: string;
|
|
value: string | number | ReactNode;
|
|
valueClassName?: string;
|
|
/** 추가 className */
|
|
className?: string;
|
|
}
|
|
|
|
export function InfoField({ label, value, valueClassName = "", className = "" }: InfoFieldProps) {
|
|
return (
|
|
<div className={`space-y-0.5 ${className}`}>
|
|
<p className="text-xs text-muted-foreground">{label}</p>
|
|
<div className={`text-sm font-medium ${valueClassName}`}>{value}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ListMobileCard({
|
|
id,
|
|
isSelected,
|
|
onToggleSelection,
|
|
onCardClick,
|
|
onClick,
|
|
showCheckbox = true,
|
|
headerBadges,
|
|
title,
|
|
statusBadge,
|
|
infoGrid,
|
|
actions,
|
|
className = "",
|
|
topContent,
|
|
bottomContent
|
|
}: ListMobileCardProps) {
|
|
const handleCardClick = onClick || onCardClick;
|
|
return (
|
|
<div
|
|
className={`border rounded-lg p-5 space-y-4 bg-white dark:bg-card transition-all cursor-pointer ${
|
|
isSelected
|
|
? 'border-blue-500 bg-blue-50/50'
|
|
: 'border-gray-200 hover:border-primary/50'
|
|
} ${className}`}
|
|
onClick={handleCardClick}
|
|
>
|
|
{/* 상단 추가 콘텐츠 */}
|
|
{topContent}
|
|
|
|
{/* 헤더: 체크박스 + 뱃지 + 제목 / 우측에 상태 뱃지 */}
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex items-start gap-3 flex-1 min-w-0">
|
|
{showCheckbox && (
|
|
<Checkbox
|
|
checked={isSelected}
|
|
onCheckedChange={onToggleSelection}
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="mt-0.5 h-5 w-5"
|
|
/>
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
{/* 헤더 뱃지들 (번호, 코드 등) */}
|
|
{headerBadges && (
|
|
<div className="flex items-center gap-2 flex-wrap mb-2">
|
|
{headerBadges}
|
|
</div>
|
|
)}
|
|
{/* 제목 */}
|
|
<h3 className="font-semibold text-gray-900 font-bold whitespace-nowrap">
|
|
{title}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 우측 상단: 상태 뱃지 */}
|
|
{statusBadge && (
|
|
<div className="shrink-0">
|
|
{statusBadge}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 구분선 */}
|
|
<Separator className="bg-gray-100" />
|
|
|
|
{/* 정보 그리드 */}
|
|
{infoGrid}
|
|
|
|
{/* 액션 버튼 - 선택된 경우만 표시 */}
|
|
{actions && (
|
|
<>
|
|
<Separator className="bg-gray-100" />
|
|
{actions}
|
|
</>
|
|
)}
|
|
|
|
{/* 하단 추가 콘텐츠 */}
|
|
{bottomContent}
|
|
</div>
|
|
);
|
|
}
|