refactor: 로딩 스피너 표준화 및 프로젝트 헬스 개선
- 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>
This commit is contained in:
@@ -47,11 +47,17 @@ export interface ListMobileCardProps {
|
||||
/** 카드 클릭 핸들러 */
|
||||
onCardClick?: () => void;
|
||||
|
||||
/** 카드 클릭 핸들러 (onCardClick 별칭) */
|
||||
onClick?: () => void;
|
||||
|
||||
/** 체크박스 표시 여부 */
|
||||
showCheckbox?: boolean;
|
||||
|
||||
/** 헤더 영역 뱃지들 (번호, 코드 등) */
|
||||
headerBadges?: ReactNode;
|
||||
|
||||
/** 카드 제목 (주요 정보) */
|
||||
title: string;
|
||||
title: string | ReactNode;
|
||||
|
||||
/** 상태 뱃지 (우측 상단) */
|
||||
statusBadge?: ReactNode;
|
||||
@@ -81,11 +87,13 @@ export interface InfoFieldProps {
|
||||
label: string;
|
||||
value: string | number | ReactNode;
|
||||
valueClassName?: string;
|
||||
/** 추가 className */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InfoField({ label, value, valueClassName = "" }: InfoFieldProps) {
|
||||
export function InfoField({ label, value, valueClassName = "", className = "" }: InfoFieldProps) {
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<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>
|
||||
@@ -97,6 +105,8 @@ export function ListMobileCard({
|
||||
isSelected,
|
||||
onToggleSelection,
|
||||
onCardClick,
|
||||
onClick,
|
||||
showCheckbox = true,
|
||||
headerBadges,
|
||||
title,
|
||||
statusBadge,
|
||||
@@ -106,6 +116,7 @@ export function ListMobileCard({
|
||||
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 ${
|
||||
@@ -113,7 +124,7 @@ export function ListMobileCard({
|
||||
? 'border-blue-500 bg-blue-50/50'
|
||||
: 'border-gray-200 hover:border-primary/50'
|
||||
} ${className}`}
|
||||
onClick={onCardClick}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
{/* 상단 추가 콘텐츠 */}
|
||||
{topContent}
|
||||
@@ -121,12 +132,14 @@ export function ListMobileCard({
|
||||
{/* 헤더: 체크박스 + 뱃지 + 제목 / 우측에 상태 뱃지 */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={onToggleSelection}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-0.5 h-5 w-5"
|
||||
/>
|
||||
{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 && (
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { ReactNode } from "react";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface PageHeaderProps {
|
||||
export interface PageHeaderProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: ReactNode;
|
||||
icon?: LucideIcon;
|
||||
versionBadge?: ReactNode;
|
||||
/** 뒤로가기 핸들러 */
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, description, actions, icon: Icon, versionBadge }: PageHeaderProps) {
|
||||
|
||||
Reference in New Issue
Block a user