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:
byeongcheolryu
2025-12-20 14:33:11 +09:00
parent c6b605200d
commit d7f491fa84
50 changed files with 666 additions and 246 deletions

View File

@@ -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 && (

View File

@@ -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) {