ERP-node/UI_개선사항_문서.md

21 KiB

ERP 시스템 UI/UX 디자인 가이드

📋 문서 목적

이 문서는 ERP 시스템의 새로운 페이지나 컴포넌트를 개발할 때 참고할 수 있는 디자인 시스템 기준안입니다. 일관된 사용자 경험을 위해 모든 개발자는 이 가이드를 따라 개발해주세요.


🎨 디자인 시스템 개요

디자인 철학

  • 일관성: 모든 페이지에서 동일한 패턴 사용
  • 명확성: 직관적이고 이해하기 쉬운 UI
  • 접근성: 모든 사용자가 쉽게 사용할 수 있도록
  • 반응성: 다양한 화면 크기에 대응

기술 스택

  • CSS Framework: Tailwind CSS
  • UI Library: shadcn/ui
  • Icons: Lucide React

📐 페이지 기본 구조

1. 표준 페이지 레이아웃

export default function YourPage() {
  return (
    <div className="min-h-screen bg-gray-50">
      <div className="w-full max-w-none px-4 py-8 space-y-8">
        {/* 페이지 제목 */}
        <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
          <div>
            <h1 className="text-3xl font-bold text-gray-900">페이지 제목</h1>
            <p className="mt-2 text-gray-600">페이지 설명</p>
          </div>
          <div className="flex gap-2">
            {/* 버튼들 */}
          </div>
        </div>

        {/* 메인 컨텐츠 */}
        <Card className="shadow-sm">
          <CardContent className="p-6">
            {/* 내용 */}
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

2. 구조 설명

최상위 래퍼

<div className="min-h-screen bg-gray-50">
  • min-h-screen: 최소 높이를 화면 전체로
  • bg-gray-50: 연한 회색 배경 (전체 페이지 기본 배경)

컨테이너

<div className="w-full max-w-none px-4 py-8 space-y-8">
  • w-full max-w-none: 전체 너비 사용
  • px-4: 좌우 패딩 1rem (16px)
  • py-8: 상하 패딩 2rem (32px)
  • space-y-8: 하위 요소 간 수직 간격 2rem

헤더 카드

<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
  <div>
    <h1 className="text-3xl font-bold text-gray-900">제목</h1>
    <p className="mt-2 text-gray-600">설명</p>
  </div>
  <div className="flex gap-2">
    {/* 버튼들 */}
  </div>
</div>

🎯 컴포넌트 디자인 기준

1. 버튼

주요 버튼 (Primary)

<Button className="bg-orange-500 hover:bg-orange-600">
  <Plus className="w-4 h-4 mr-2" />
  버튼 텍스트
</Button>

보조 버튼 (Secondary)

<Button variant="outline" size="sm">
  <RefreshCw className="w-4 h-4 mr-2" />
  새로고침
</Button>

위험 버튼 (Danger)

<Button 
  variant="ghost" 
  className="text-red-500 hover:text-red-600"
>
  <Trash2 className="w-4 h-4" />
  삭제
</Button>

2. 카드 (Card)

기본 카드

<Card className="shadow-sm">
  <CardHeader>
    <CardTitle>카드 제목</CardTitle>
  </CardHeader>
  <CardContent className="p-6">
    {/* 내용 */}
  </CardContent>
</Card>

강조 카드

<Card className="bg-gradient-to-r from-orange-50 to-amber-50 border-orange-200 shadow-sm">
  <CardHeader>
    <CardTitle className="flex items-center">
      <Icon className="w-5 h-5 mr-2 text-orange-500" />
      제목
    </CardTitle>
  </CardHeader>
  <CardContent>
    <p className="text-gray-700">내용</p>
  </CardContent>
</Card>

3. 테이블

기본 테이블 구조

<Card className="shadow-sm">
  <CardContent className="p-6">
    <table className="w-full">
      <thead>
        <tr className="border-b border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70">
          <th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
            컬럼명
          </th>
        </tr>
      </thead>
      <tbody>
        <tr className="h-12 border-b border-gray-100/60 hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60">
          <td className="px-6 py-4 text-sm text-gray-600">
            데이터
          </td>
        </tr>
      </tbody>
    </table>
  </CardContent>
</Card>

4. 폼 (Form)

입력 필드

<div className="space-y-2">
  <label className="block text-sm font-medium text-gray-700">
    라벨
  </label>
  <input
    type="text"
    className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all duration-200"
  />
</div>

셀렉트

<Select>
  <SelectTrigger className="w-48">
    <SelectValue placeholder="선택하세요" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="1">옵션 1</SelectItem>
    <SelectItem value="2">옵션 2</SelectItem>
  </SelectContent>
</Select>

5. 빈 상태 (Empty State)

<Card className="text-center py-16 bg-white shadow-sm">
  <CardContent className="pt-6">
    <Icon className="w-16 h-16 mx-auto mb-4 text-gray-300" />
    <p className="text-gray-500 mb-4">데이터가 없습니다</p>
    <Button className="bg-orange-500 hover:bg-orange-600">
      <Plus className="w-4 h-4 mr-2" />
      추가하기
    </Button>
  </CardContent>
</Card>

6. 로딩 상태

<Card className="shadow-sm">
  <CardContent className="flex justify-center items-center py-16">
    <Loader2 className="w-8 h-8 animate-spin text-orange-500" />
  </CardContent>
</Card>

🎨 색상 시스템

주 색상 (Primary)

orange-50   #fff7ed    /* 매우 연한 배경 */
orange-100  #ffedd5    /* 연한 배경 */
orange-500  #f97316    /* 주요 버튼, 강조 */
orange-600  #ea580c    /* 버튼 호버 */

회색 (Gray)

gray-50     #f9fafb    /* 페이지 배경 */
gray-100    #f3f4f6    /* 카드 내부 구분 */
gray-200    #e5e7eb    /* 테두리 */
gray-300    #d1d5db    /* 입력 필드 테두리 */
gray-500    #6b7280    /* 보조 텍스트 */
gray-600    #4b5563    /* 일반 텍스트 */
gray-700    #374151    /* 라벨, 헤더 */
gray-800    #1f2937    /* 제목 */
gray-900    #111827    /* 주요 제목 */

상태 색상

/* 성공 */
green-100   #dcfce7
green-500   #22c55e
green-700   #15803d

/* 경고 */
red-100     #fee2e2
red-500     #ef4444
red-600     #dc2626

/* 정보 */
blue-50     #eff6ff
blue-100    #dbeafe
blue-500    #3b82f6

📏 간격 시스템

Spacing Scale

space-y-2   0.5rem (8px)   /* 폼 요소 간 간격 */
space-y-4   1rem (16px)    /* 섹션 내부 간격 */
space-y-6   1.5rem (24px)  /* 카드 내부 큰 간격 */
space-y-8   2rem (32px)    /* 페이지 주요 섹션 간격 */

gap-2       0.5rem (8px)   /* 버튼 그룹 간격 */
gap-4       1rem (16px)    /* 카드 그리드 간격 */
gap-6       1.5rem (24px)  /* 큰 카드 그리드 간격 */

Padding

p-2         0.5rem (8px)   /* 작은 요소 */
p-4         1rem (16px)    /* 일반 요소 */
p-6         1.5rem (24px)  /* 카드, 헤더 */
p-8         2rem (32px)    /* 큰 영역 */

px-3        좌우 0.75rem   /* 입력 필드 */
px-4        좌우 1rem      /* 버튼 */
px-6        좌우 1.5rem    /* 테이블 셀 */

py-2        상하 0.5rem    /* 버튼 */
py-4        상하 1rem      /* 입력 필드 */
py-8        상하 2rem      /* 페이지 컨테이너 */

📝 타이포그래피

제목 (Headings)

/* 페이지 제목 */
text-3xl font-bold text-gray-900
/* 예: 30px, Bold, #111827 */

/* 섹션 제목 */
text-2xl font-bold text-gray-900
/* 예: 24px, Bold */

/* 카드 제목 */
text-lg font-semibold text-gray-800
/* 예: 18px, Semi-bold */

/* 작은 제목 */
text-base font-medium text-gray-700
/* 예: 16px, Medium */

본문 (Body Text)

/* 일반 텍스트 */
text-sm text-gray-600
/* 14px, #4b5563 */

/* 보조 설명 */
text-sm text-gray-500
/* 14px, #6b7280 */

/* 라벨 */
text-sm font-medium text-gray-700
/* 14px, Medium */

🎭 인터랙션 패턴

호버 효과

/* 버튼 호버 */
hover:bg-orange-600
hover:shadow-md

/* 카드 호버 */
hover:shadow-lg transition-shadow

/* 테이블 행 호버 */
hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60

포커스 효과

/* 입력 필드 포커스 */
focus:outline-none 
focus:ring-2 
focus:ring-orange-500 
focus:border-orange-500

전환 효과

/* 일반 전환 */
transition-all duration-200

/* 그림자 전환 */
transition-shadow

/* 색상 전환 */
transition-colors duration-200

🔲 그리드 시스템

반응형 그리드

{/* 1열 → 2열 → 3열 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  {/* 카드들 */}
</div>

{/* 1열 → 2열 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  {/* 항목들 */}
</div>

브레이크포인트

sm:   640px  @media (min-width: 640px)
md:   768px  @media (min-width: 768px)
lg:   1024px @media (min-width: 1024px)
xl:   1280px @media (min-width: 1280px)
2xl:  1536px @media (min-width: 1536px)

🎯 실전 예제

예제 1: 관리 페이지 (데이터 있음)

export default function ManagementPage() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  return (
    <div className="min-h-screen bg-gray-50">
      <div className="w-full max-w-none px-4 py-8 space-y-8">
        {/* 헤더 */}
        <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
          <div>
            <h1 className="text-3xl font-bold text-gray-900">데이터 관리</h1>
            <p className="mt-2 text-gray-600">시스템 데이터를 관리합니다</p>
          </div>
          <div className="flex gap-2">
            <Button variant="outline" size="sm" onClick={loadData}>
              <RefreshCw className="w-4 h-4 mr-2" />
              새로고침
            </Button>
            <Button className="bg-orange-500 hover:bg-orange-600">
              <Plus className="w-4 h-4 mr-2" />
              새로 추가
            </Button>
          </div>
        </div>

        {/* 통계 카드 */}
        <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
          <Card className="shadow-sm">
            <CardContent className="p-6">
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-gray-500"> 개수</p>
                  <p className="text-2xl font-bold text-gray-900">156</p>
                </div>
                <div className="bg-blue-100 p-3 rounded-lg">
                  <Database className="w-6 h-6 text-blue-500" />
                </div>
              </div>
            </CardContent>
          </Card>
          {/* 나머지 통계 카드들... */}
        </div>

        {/* 데이터 테이블 */}
        <Card className="shadow-sm">
          <CardContent className="p-6">
            <table className="w-full">
              <thead>
                <tr className="border-b border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70">
                  <th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
                    이름
                  </th>
                  <th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
                    상태
                  </th>
                  <th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
                    작업
                  </th>
                </tr>
              </thead>
              <tbody>
                {data.map((item) => (
                  <tr 
                    key={item.id}
                    className="h-12 border-b border-gray-100/60 hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60"
                  >
                    <td className="px-6 py-4 text-sm text-gray-600">
                      {item.name}
                    </td>
                    <td className="px-6 py-4">
                      <span className="px-2 py-1 text-xs rounded bg-green-100 text-green-700">
                        활성
                      </span>
                    </td>
                    <td className="px-6 py-4">
                      <div className="flex gap-2">
                        <Button size="sm" variant="outline">
                          수정
                        </Button>
                        <Button size="sm" variant="ghost" className="text-red-500">
                          삭제
                        </Button>
                      </div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

예제 2: 빈 상태 페이지

export default function EmptyStatePage() {
  return (
    <div className="min-h-screen bg-gray-50">
      <div className="w-full max-w-none px-4 py-8 space-y-8">
        {/* 헤더 */}
        <div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
          <div>
            <h1 className="text-3xl font-bold text-gray-900">데이터 관리</h1>
            <p className="mt-2 text-gray-600">시스템 데이터를 관리합니다</p>
          </div>
          <Button className="bg-orange-500 hover:bg-orange-600">
            <Plus className="w-4 h-4 mr-2" />
            새로 추가
          </Button>
        </div>

        {/* 빈 상태 */}
        <Card className="text-center py-16 bg-white shadow-sm">
          <CardContent className="pt-6">
            <Database className="w-16 h-16 mx-auto mb-4 text-gray-300" />
            <p className="text-gray-500 mb-4">아직 등록된 데이터가 없습니다</p>
            <Button className="bg-orange-500 hover:bg-orange-600">
              <Plus className="w-4 h-4 mr-2" />
               데이터 추가하기
            </Button>
          </CardContent>
        </Card>

        {/* 안내 정보 */}
        <Card className="bg-gradient-to-r from-orange-50 to-amber-50 border-orange-200 shadow-sm">
          <CardHeader>
            <CardTitle className="text-lg flex items-center">
              <Info className="w-5 h-5 mr-2 text-orange-500" />
              데이터 관리 안내
            </CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-gray-700 mb-4">
              💡 데이터를 추가하여 시스템을 사용해보세요!
            </p>
            <ul className="space-y-2 text-sm text-gray-600">
              <li className="flex items-start">
                <span className="text-orange-500 mr-2"></span>
                <span>기능 설명 1</span>
              </li>
              <li className="flex items-start">
                <span className="text-orange-500 mr-2"></span>
                <span>기능 설명 2</span>
              </li>
            </ul>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

체크리스트

새 페이지 만들 때

  • min-h-screen bg-gray-50 래퍼 사용
  • 헤더 카드 (bg-white rounded-lg shadow-sm border p-6) 포함
  • 제목은 text-3xl font-bold text-gray-900
  • 설명은 mt-2 text-gray-600
  • 주요 버튼은 bg-orange-500 hover:bg-orange-600
  • 카드는 shadow-sm 클래스 포함
  • 간격은 space-y-8 사용

새 컴포넌트 만들 때

  • 일관된 패딩 사용 (p-4, p-6)
  • 호버 효과 추가
  • 전환 애니메이션 적용 (transition-all duration-200)
  • 적절한 아이콘 사용 (Lucide React)
  • 반응형 디자인 고려 (md:, lg:)

📚 참고 자료

Tailwind CSS 공식 문서

shadcn/ui 컴포넌트

Lucide 아이콘


📧 메일 관리 시스템 UI 개선사항

최근 업데이트 (2025-01-02)

1. 메일 발송 페이지 헤더 개선

변경 전:

<div className="flex items-center justify-between">
  <div className="flex items-center gap-3">
    <div className="p-3 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-lg">
      <Send className="w-6 h-6 text-blue-600" />
    </div>
    <div>
      <h1 className="text-2xl font-bold text-gray-900">메일 발송</h1>
      <p className="text-sm text-gray-500">설명</p>
    </div>
  </div>
</div>

변경 후 (표준 헤더 카드 적용):

<Card>
  <CardHeader>
    <CardTitle className="text-2xl">메일 발송</CardTitle>
    <p className="text-sm text-muted-foreground mt-1">
      템플릿을 선택하거나 직접 작성하여 메일을 발송하세요
    </p>
  </CardHeader>
</Card>

개선 사항:

  • 불필요한 아이콘 제거 (종이비행기)
  • 표준 Card 컴포넌트 사용으로 통일감 향상
  • 다른 페이지와 동일한 헤더 스타일 적용

2. 메일 내용 입력 개선

변경 전:

<Textarea placeholder="메일 내용을 html로 작성하세요" />

변경 후:

<Textarea 
  placeholder="메일 내용을 입력하세요

줄바꿈은 자동으로 처리됩니다."
/>
<p className="text-xs text-gray-500 mt-1">
  💡 일반 텍스트로 작성하면 자동으로 메일 형식으로 변환됩니다
</p>

개선 사항:

  • HTML 지식 없이도 사용 가능
  • 일반 텍스트 입력 후 자동 HTML 변환
  • 사용자 친화적인 안내 메시지

3. CC/BCC 기능 추가

구현 내용:

{/* To 태그 입력 */}
<EmailTagInput
  tags={to}
  onTagsChange={setTo}
  placeholder="받는 사람 이메일"
/>

{/* CC 태그 입력 */}
<EmailTagInput
  tags={cc}
  onTagsChange={setCc}
  placeholder="참조 (선택사항)"
/>

{/* BCC 태그 입력 */}
<EmailTagInput
  tags={bcc}
  onTagsChange={setBcc}
  placeholder="숨은참조 (선택사항)"
/>

특징:

  • 이메일 주소를 태그 형태로 시각화
  • 쉼표로 구분하여 입력 가능
  • 개별 삭제 가능

4. 파일 첨부 기능 (Phase 1 완료)

백엔드 구현:

// multer 설정
export const uploadMailAttachment = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB 제한
    files: 5, // 최대 5개 파일
  },
});

// 발송 API
router.post(
  '/simple',
  uploadMailAttachment.array('attachments', 5),
  (req, res) => mailSendSimpleController.sendMail(req, res)
);

보안 기능:

  • 위험한 파일 확장자 차단 (.exe, .bat, .cmd, .sh 등)
  • 파일 크기 제한 (10MB)
  • 파일 개수 제한 (최대 5개)
  • 안전한 파일명 생성

프론트엔드 구현 예정 (Phase 1-3):

  • 드래그 앤 드롭 파일 업로드
  • 첨부된 파일 목록 표시
  • 파일 삭제 기능
  • 미리보기에 첨부파일 정보 표시

5. 향후 작업 계획

Phase 2: 보낸메일함 백엔드

  • 발송 이력 자동 저장 (JSON 파일)
  • 발송 상태 관리 (성공/실패)
  • 발송 이력 조회 API

Phase 3: 보낸메일함 프론트엔드

  • /admin/mail/sent 페이지
  • 발송 목록 테이블
  • 상세보기 모달
  • 재전송 기능

Phase 4: 대시보드 통합

  • 대시보드에 "보낸메일함" 링크
  • 실제 발송 통계 연동
  • 최근 활동 목록

메일 시스템 UI 가이드

이메일 태그 입력

// 이메일 주소를 시각적으로 표시
<div className="flex flex-wrap gap-2">
  {tags.map((tag, index) => (
    <div 
      key={index}
      className="flex items-center gap-1 px-3 py-1.5 bg-blue-100 text-blue-700 rounded-md text-sm"
    >
      <Mail className="w-3 h-3" />
      {tag}
      <button onClick={() => removeTag(index)}>
        <X className="w-3 h-3" />
      </button>
    </div>
  ))}
</div>

파일 첨부 영역 (예정)

<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 hover:border-orange-400 transition-colors">
  <input type="file" multiple className="hidden" />
  <div className="text-center">
    <Upload className="w-12 h-12 mx-auto text-gray-400" />
    <p className="mt-2 text-sm text-gray-600">
      파일을 드래그하거나 클릭하여 선택하세요
    </p>
    <p className="text-xs text-gray-500 mt-1">
      최대 5,  10MB 이하
    </p>
  </div>
</div>

발송 성공 토스트

<div className="fixed top-4 right-4 bg-white rounded-lg shadow-lg border border-green-200 p-4">
  <div className="flex items-center gap-3">
    <CheckCircle className="w-5 h-5 text-green-500" />
    <div>
      <p className="font-medium text-gray-900">메일이 발송되었습니다</p>
      <p className="text-sm text-gray-600">
        {to.length}명에게 전송 완료
      </p>
    </div>
  </div>
</div>

이 가이드를 따라 개발하면 일관되고 아름다운 UI를 만들 수 있습니다! 🎨