diff --git a/.cursor/rules/admin-page-style-guide.mdc b/.cursor/rules/admin-page-style-guide.mdc new file mode 100644 index 00000000..d0cdaf23 --- /dev/null +++ b/.cursor/rules/admin-page-style-guide.mdc @@ -0,0 +1,749 @@ +--- +description: 관리자 페이지 표준 스타일 가이드 - shadcn/ui 기반 일관된 디자인 시스템 +globs: **/app/(main)/admin/**/*.tsx,**/components/admin/**/*.tsx +--- + +# 관리자 페이지 표준 스타일 가이드 + +이 가이드는 관리자 페이지의 일관된 UI/UX를 위한 표준 스타일 규칙입니다. +모든 관리자 페이지는 이 가이드를 따라야 합니다. + +## 1. 페이지 레이아웃 구조 + +### 기본 페이지 템플릿 + +```tsx +export default function AdminPage() { + return ( +
+
+ {/* 페이지 헤더 */} +
+

페이지 제목

+

페이지 설명

+
+ + {/* 메인 컨텐츠 */} + +
+ + {/* Scroll to Top 버튼 (모바일/태블릿 전용) */} + +
+ ); +} +``` + +**필수 적용 사항:** + +- 최상위: `flex min-h-screen flex-col bg-background` +- 컨텐츠 영역: `space-y-6 p-6` (24px 좌우 여백, 24px 간격) +- 헤더 구분선: `border-b pb-4` (테두리 박스 사용 금지) +- Scroll to Top: 모든 관리자 페이지에 포함 + +## 2. Color System (색상 시스템) + +### CSS Variables 사용 (하드코딩 금지) + +```tsx +// ❌ 잘못된 예시 +
+ +// ✅ 올바른 예시 +
+
+
+``` + +**표준 색상 토큰:** + +- `bg-background` / `text-foreground`: 기본 배경/텍스트 +- `bg-card` / `text-card-foreground`: 카드 배경/텍스트 +- `bg-muted` / `text-muted-foreground`: 보조 배경/텍스트 +- `bg-primary` / `text-primary`: 메인 액션 +- `bg-destructive` / `text-destructive`: 삭제/에러 +- `border-border`: 테두리 +- `ring-ring`: 포커스 링 + +## 3. Typography (타이포그래피) + +### 표준 텍스트 크기와 가중치 + +```tsx +// 페이지 제목 +

+ +// 섹션 제목 +

+

+

+ +// 본문 텍스트 +

+ +// 보조 텍스트 +

+

+ +// 라벨 +

// 24px + +// 섹션 레벨 간격 +
// 16px + +// 필드 레벨 간격 +
// 8px + +// 패딩 +
// 24px (카드) +
// 16px (내부 섹션) + +// 갭 +
// 16px (flex/grid) +
// 8px (버튼 그룹) +``` + +## 5. 검색 툴바 (Toolbar) + +### 패턴 A: 통합 검색 영역 (권장) + +```tsx +
+ {/* 검색 및 액션 영역 */} +
+ {/* 검색 영역 */} +
+ {/* 통합 검색 */} +
+
+ + +
+
+ + {/* 고급 검색 토글 */} + +
+ + {/* 액션 버튼 영역 */} +
+
+ 총{" "} + + {count.toLocaleString()} + {" "} + 건 +
+ +
+
+ + {/* 고급 검색 옵션 */} + {showAdvanced && ( +
+
+

고급 검색 옵션

+

설명

+
+
+ +
+
+ )} +
+``` + +### 패턴 B: 제목 + 검색 + 버튼 한 줄 (공간 효율적) + +```tsx +{ + /* 상단 헤더: 제목 + 검색 + 버튼 */ +} +
+ {/* 왼쪽: 제목 */} +

페이지 제목

+ + {/* 오른쪽: 검색 + 버튼 */} +
+ {/* 필터 선택 */} +
+ +
+ + {/* 검색 입력 */} +
+ +
+ + {/* 초기화 버튼 */} + + + {/* 주요 액션 버튼 */} + + + {/* 조건부 버튼 (선택 시) */} + {selectedCount > 0 && ( + + )} +
+
; +``` + +**필수 적용 사항:** + +- ❌ 검색 영역에 박스/테두리 사용 금지 +- ✅ 검색창 권장 너비: `w-full sm:w-[240px]` ~ `sm:w-[400px]` +- ✅ 필터/Select 권장 너비: `w-full sm:w-[160px]` ~ `sm:w-[200px]` +- ✅ 고급 검색 필드: placeholder만 사용 (라벨 제거) +- ✅ 검색 아이콘: `Search` (lucide-react) +- ✅ Input/Select 높이: `h-10` (40px) +- ✅ 상단 헤더에 `relative` 추가 (드롭다운 표시용) + +## 6. Button (버튼) + +### 표준 버튼 variants와 크기 + +```tsx +// Primary 액션 + + +// Secondary 액션 + + +// Ghost 버튼 (아이콘 전용) + + +// Destructive + +``` + +**표준 크기:** + +- `h-10`: 기본 버튼 (40px) +- `h-9`: 작은 버튼 (36px) +- `h-8`: 아이콘 버튼 (32px) + +**아이콘 크기:** + +- `h-4 w-4`: 버튼 내 아이콘 (16px) + +## 7. Input (입력 필드) + +### 표준 Input 스타일 + +```tsx +// 기본 + + +// 검색 (아이콘 포함) +
+ + +
+ +// 로딩/액티브 + + +// 비활성화 + +``` + +**필수 적용 사항:** + +- 높이: `h-10` (40px) +- 텍스트: `text-sm` +- 포커스: 자동 적용 (`ring-2 ring-ring`) + +## 8. Table & Card (테이블과 카드) + +### 반응형 테이블/카드 구조 + +```tsx +// 실제 데이터 렌더링 +return ( + <> + {/* 데스크톱 테이블 뷰 (lg 이상) */} +
+ + + + 컬럼 + + + + + 데이터 + + +
+
+ + {/* 모바일/태블릿 카드 뷰 (lg 미만) */} +
+ {items.map((item) => ( +
+ {/* 헤더 */} +
+
+

{item.name}

+

{item.id}

+
+ +
+ + {/* 정보 */} +
+
+ 필드 + {item.value} +
+
+ + {/* 액션 */} +
+ +
+
+ ))} +
+ +); +``` + +**테이블 표준:** + +- 헤더: `h-12` (48px), `bg-muted/50`, `font-semibold` +- 데이터 행: `h-16` (64px), `hover:bg-muted/50` +- 텍스트: `text-sm` + +**카드 표준:** + +- 컨테이너: `rounded-lg border bg-card p-4 shadow-sm` +- 헤더 제목: `text-base font-semibold` +- 부제목: `text-sm text-muted-foreground` +- 정보 라벨: `text-sm text-muted-foreground` +- 정보 값: `text-sm font-medium` +- 버튼: `h-9 flex-1 gap-2 text-sm` + +## 9. Loading States (로딩 상태) + +### Skeleton UI 패턴 + +```tsx +// 테이블 스켈레톤 (데스크톱) +
+ + ... + + {Array.from({ length: 10 }).map((_, index) => ( + + +
+
+
+ ))} +
+
+
+ +// 카드 스켈레톤 (모바일/태블릿) +
+ {Array.from({ length: 6 }).map((_, index) => ( +
+
+
+
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+
+
+ ))} +
+
+ ))} +
+``` + +## 10. Empty States (빈 상태) + +### 표준 Empty State + +```tsx +
+
+

데이터가 없습니다.

+
+
+``` + +## 11. Error States (에러 상태) + +### 표준 에러 메시지 + +```tsx +
+
+

+ 오류가 발생했습니다 +

+ +
+

{errorMessage}

+
+``` + +## 12. Responsive Design (반응형) + +### Breakpoints + +- `sm`: 640px (모바일 가로/태블릿) +- `md`: 768px (태블릿) +- `lg`: 1024px (노트북) +- `xl`: 1280px (데스크톱) + +### 모바일 우선 패턴 + +```tsx +// 레이아웃 +
+ +// 그리드 +
+ +// 검색창 +
+ +// 테이블/카드 전환 +
{/* 데스크톱 테이블 */} +
{/* 모바일 카드 */} + +// 간격 +
+
+``` + +## 13. 좌우 레이아웃 (Side-by-Side Layout) + +### 사이드바 + 메인 영역 구조 + +```tsx +
+ {/* 좌측 사이드바 (20-30%) */} +
+
+

사이드바 제목

+ + {/* 사이드바 컨텐츠 */} +
+
+

항목

+

설명

+
+
+
+
+ + {/* 우측 메인 영역 (70-80%) */} +
+
+

메인 제목

+ + {/* 메인 컨텐츠 */} +
{/* 컨텐츠 */}
+
+
+
+``` + +**필수 적용 사항:** + +- ✅ 좌우 구분: `border-r` 사용 (세로 구분선) +- ✅ 간격: `gap-6` (24px) +- ✅ 사이드바 패딩: `pr-6` (오른쪽 24px) +- ✅ 메인 영역 패딩: `pl-0` (gap으로 간격 확보) +- ✅ 비율: 20:80 또는 30:70 +- ❌ 과도한 구분선 사용 금지 (세로 구분선 1개만) +- ❌ 사이드바와 메인 영역 각각에 추가 border 금지 + +## 14. Custom Dropdown (커스텀 드롭다운) + +### 커스텀 Select/Dropdown 구조 + +```tsx +{ + /* 드롭다운 컨테이너 */ +} +
+
+ {/* 트리거 버튼 */} + + + {/* 드롭다운 메뉴 */} + {isOpen && ( +
+ {/* 검색 (선택사항) */} +
+ setSearchText(e.target.value)} + className="h-8 text-sm" + onClick={(e) => e.stopPropagation()} + /> +
+ + {/* 옵션 목록 */} +
+ {options.map((option) => ( +
{ + setValue(option.value); + setIsOpen(false); + }} + > + {option.label} +
+ ))} +
+
+ )} +
+
; +``` + +**필수 적용 사항:** + +- ✅ z-index: `z-[100]` (다른 요소 위에 표시) +- ✅ 그림자: `shadow-lg` (명확한 레이어 구분) +- ✅ 최소 너비: `min-w-[200px]` (내용이 잘리지 않도록) +- ✅ 최대 높이: `max-h-48` (스크롤 가능) +- ✅ 애니메이션: 화살표 아이콘 회전 (`rotate-180`) +- ✅ 부모 요소: `relative` 클래스 필요 +- ⚠️ 부모에 `overflow-hidden` 사용 시 드롭다운 잘림 주의 + +**드롭다운이 잘릴 때 해결방법:** + +```tsx +// 부모 요소의 overflow 제거 +
// overflow-hidden 제거 + +// 또는 상단 헤더에 relative 추가 +
// 드롭다운 포지셔닝 기준점 +``` + +## 15. Scroll to Top Button + +### 모바일/태블릿 전용 버튼 + +```tsx +import { ScrollToTop } from "@/components/common/ScrollToTop"; + +// 페이지에 추가 +; +``` + +**특징:** + +- 데스크톱에서 숨김 (`lg:hidden`) +- 스크롤 200px 이상 시 나타남 +- 부드러운 페이드 인/아웃 애니메이션 +- 오른쪽 하단 고정 위치 +- 원형 디자인 (`rounded-full`) + +## 14. Accessibility (접근성) + +### 필수 적용 사항 + +```tsx +// Label과 Input 연결 + + + +// 버튼에 aria-label + + +// Switch에 aria-label + + +// 포커스 표시 (자동 적용) +focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring +``` + +## 15. Class 순서 (일관성) + +### 표준 클래스 작성 순서 + +1. Layout: `flex`, `grid`, `block` +2. Position: `fixed`, `absolute`, `relative` +3. Sizing: `w-full`, `h-10` +4. Spacing: `p-4`, `m-2`, `gap-4` +5. Typography: `text-sm`, `font-medium` +6. Colors: `bg-primary`, `text-white` +7. Border: `border`, `rounded-md` +8. Effects: `shadow-sm`, `opacity-50` +9. States: `hover:`, `focus:`, `disabled:` +10. Responsive: `sm:`, `md:`, `lg:` + +## 16. 금지 사항 + +### ❌ 절대 사용하지 말 것 + +1. 하드코딩된 색상 (`bg-gray-50`, `text-blue-500` 등) +2. 인라인 스타일로 색상 지정 (`style={{ color: '#3b82f6' }}`) +3. 포커스 스타일 제거 (`outline-none`만 단독 사용) +4. 중첩된 박스 (Card 안에 Card, Border 안에 Border) +5. 검색 영역에 불필요한 박스/테두리 +6. 검색 필드에 라벨 (placeholder만 사용) +7. 반응형 무시 (데스크톱 전용 스타일) +8. **이모지 사용** (사용자가 명시적으로 요청하지 않는 한 절대 사용 금지) +9. 과도한 구분선 사용 (최소한으로 유지) +10. 드롭다운 부모에 `overflow-hidden` (잘림 발생) + +## 17. 체크리스트 + +새로운 관리자 페이지 작성 시 다음을 확인하세요: + +### 페이지 레벨 + +- [ ] `bg-background` 사용 (하드코딩 금지) +- [ ] `space-y-6 p-6` 구조 +- [ ] 페이지 헤더에 `border-b pb-4` +- [ ] `ScrollToTop` 컴포넌트 포함 + +### 검색 툴바 + +- [ ] 박스/테두리 없음 +- [ ] 검색창 최대 너비 `sm:w-[400px]` +- [ ] 고급 검색 필드에 라벨 없음 (placeholder만) +- [ ] 반응형 레이아웃 적용 + +### 테이블/카드 + +- [ ] 데스크톱: 테이블 (`hidden lg:block`) +- [ ] 모바일: 카드 (`lg:hidden`) +- [ ] 표준 높이와 간격 적용 +- [ ] 로딩/Empty 상태 구현 + +### 버튼 + +- [ ] 표준 variants 사용 +- [ ] 표준 높이: `h-10`, `h-9`, `h-8` +- [ ] 아이콘 크기: `h-4 w-4` +- [ ] `gap-2`로 아이콘과 텍스트 간격 + +### 반응형 + +- [ ] 모바일 우선 디자인 +- [ ] Breakpoints 적용 (`sm:`, `lg:`) +- [ ] 테이블/카드 전환 +- [ ] Scroll to Top 버튼 + +### 접근성 + +- [ ] Label `htmlFor` / Input `id` 연결 +- [ ] 버튼 `aria-label` +- [ ] Switch `aria-label` +- [ ] 포커스 표시 유지 + +## 참고 파일 + +완성된 예시: + +### 기본 패턴 + +- [사용자 관리 페이지]() - 기본 페이지 구조 +- [검색 툴바](mdc:frontend/components/admin/UserToolbar.tsx) - 패턴 A (통합 검색) +- [테이블/카드](mdc:frontend/components/admin/UserTable.tsx) - 반응형 테이블/카드 +- [Scroll to Top](mdc:frontend/components/common/ScrollToTop.tsx) - 스크롤 버튼 + +### 고급 패턴 + +- [메뉴 관리 페이지]() - 좌우 레이아웃 + 패턴 B (제목+검색+버튼) +- [메뉴 관리 컴포넌트](mdc:frontend/components/admin/MenuManagement.tsx) - 커스텀 드롭다운 + 좌우 레이아웃 diff --git a/docs/ADMIN_STYLE_GUIDE_EXAMPLE.md b/docs/ADMIN_STYLE_GUIDE_EXAMPLE.md new file mode 100644 index 00000000..2ff7f4e1 --- /dev/null +++ b/docs/ADMIN_STYLE_GUIDE_EXAMPLE.md @@ -0,0 +1,435 @@ +# 관리자 페이지 스타일 가이드 적용 예시 + +## 개요 + +사용자 관리 페이지를 예시로 shadcn/ui 스타일 가이드에 맞춰 재작성했습니다. +이 예시를 기준으로 다른 관리자 페이지들도 일관된 스타일로 통일할 수 있습니다. + +## 적용된 주요 원칙 + +### 1. Color System (색상 시스템) + +**CSS Variables 사용 (하드코딩된 색상 금지)** +```tsx +// ❌ 잘못된 예시 +
+ +// ✅ 올바른 예시 +
+
+
+
+
+``` + +**적용 사례:** +- 페이지 배경: `bg-background` +- 카드 배경: `bg-card` +- 보조 텍스트: `text-muted-foreground` +- 주요 액션: `text-primary`, `border-primary` +- 에러 메시지: `text-destructive`, `bg-destructive/10` + +### 2. Typography (타이포그래피) + +**일관된 폰트 크기와 가중치** +```tsx +// 페이지 제목 +

사용자 관리

+ +// 섹션 제목 +

고급 검색 옵션

+ +// 본문 텍스트 +

설명 텍스트

+ +// 라벨 + + +// 보조 텍스트 +

도움말

+``` + +### 3. Spacing System (간격) + +**일관된 간격 사용 (4px 기준)** +```tsx +// 컴포넌트 간 간격 +
// 24px (페이지 레벨) +
// 16px (섹션 레벨) +
// 8px (필드 레벨) + +// 패딩 +
// 24px (카드) +
// 16px (내부 섹션) + +// 갭 +
// 16px (flex/grid) +
// 8px (버튼 그룹) +``` + +### 4. Border & Radius (테두리 및 둥근 모서리) + +**표준 radius 사용** +```tsx +// 카드/패널 +
+ +// 입력 필드 + + +// 버튼 + + +// Secondary 액션 + + +// Ghost 버튼 (아이콘 전용) + +``` + +**크기 표준:** +- `h-10`: 기본 버튼 (40px) +- `h-9`: 작은 버튼 (36px) +- `h-8`: 아이콘 버튼 (32px) + +### 6. Input States (입력 필드 상태) + +**표준 Input 스타일** +```tsx +// 기본 + + +// 포커스 (자동 적용) +// focus:ring-2 focus:ring-ring + +// 로딩/액티브 + + +// 비활성화 + +``` + +### 7. Form Structure (폼 구조) + +**표준 필드 구조** +```tsx +
+ + +

+ 도움말 텍스트 +

+
+``` + +### 8. Table Structure (테이블 구조) + +**표준 테이블 스타일** +```tsx +
+ + + + + 컬럼명 + + + + + + + 데이터 + + + +
+
+``` + +**높이 표준:** +- 헤더: `h-12` (48px) +- 데이터 행: `h-16` (64px) + +### 9. Loading States (로딩 상태) + +**Skeleton UI** +```tsx +
+``` + +### 10. Empty States (빈 상태) + +**표준 Empty State** +```tsx + +
+

등록된 데이터가 없습니다.

+
+
+``` + +### 11. Error States (에러 상태) + +**표준 에러 메시지** +```tsx +
+
+

오류가 발생했습니다

+ +
+

{errorMessage}

+
+``` + +### 12. Responsive Design (반응형) + +**모바일 우선 접근** +```tsx +// 레이아웃 +
+ +// 그리드 +
+ +// 텍스트 +

+ +// 간격 +
+``` + +### 13. Accessibility (접근성) + +**필수 적용 사항** +```tsx +// Label과 Input 연결 + + + +// 버튼에 aria-label + + +// Switch에 aria-label + +``` + +## 페이지 구조 템플릿 + +### Page Component +```tsx +export default function AdminPage() { + return ( +
+
+ {/* 페이지 헤더 */} +
+

페이지 제목

+

페이지 설명

+
+ + {/* 메인 컨텐츠 */} + +
+
+ ); +} +``` + +### Toolbar Component +```tsx +export function Toolbar() { + return ( +
+ {/* 검색 영역 */} +
+
+ {/* 검색 입력 */} +
+
+ + +
+
+ + {/* 버튼 */} + +
+
+ + {/* 액션 버튼 영역 */} +
+
+ 총 {count.toLocaleString()} 건 +
+ + +
+
+ ); +} +``` + +## 적용해야 할 다른 관리자 페이지 + +### 우선순위 1 (핵심 페이지) +- [ ] 메뉴 관리 (`/admin/menu`) +- [ ] 공통코드 관리 (`/admin/commonCode`) +- [ ] 회사 관리 (`/admin/company`) +- [ ] 테이블 관리 (`/admin/tableMng`) + +### 우선순위 2 (자주 사용하는 페이지) +- [ ] 외부 연결 관리 (`/admin/external-connections`) +- [ ] 외부 호출 설정 (`/admin/external-call-configs`) +- [ ] 배치 관리 (`/admin/batch-management`) +- [ ] 레이아웃 관리 (`/admin/layouts`) + +### 우선순위 3 (기타 관리 페이지) +- [ ] 템플릿 관리 (`/admin/templates`) +- [ ] 표준 관리 (`/admin/standards`) +- [ ] 다국어 관리 (`/admin/i18n`) +- [ ] 수집 관리 (`/admin/collection-management`) + +## 체크리스트 + +각 페이지 작업 시 다음을 확인하세요: + +### 레이아웃 +- [ ] `bg-background` 사용 (하드코딩된 색상 없음) +- [ ] `container mx-auto space-y-6 p-6` 구조 +- [ ] 페이지 헤더에 `border-b pb-4` + +### 색상 +- [ ] CSS Variables만 사용 (`bg-card`, `text-muted-foreground` 등) +- [ ] `bg-gray-*`, `text-gray-*` 등 하드코딩 제거 + +### 타이포그래피 +- [ ] 페이지 제목: `text-3xl font-bold tracking-tight` +- [ ] 섹션 제목: `text-sm font-semibold` +- [ ] 본문: `text-sm` +- [ ] 보조 텍스트: `text-xs text-muted-foreground` + +### 간격 +- [ ] 페이지 레벨: `space-y-6` +- [ ] 섹션 레벨: `space-y-4` +- [ ] 필드 레벨: `space-y-2` +- [ ] 카드 패딩: `p-4` 또는 `p-6` + +### 버튼 +- [ ] 표준 variants 사용 (`default`, `outline`, `ghost`) +- [ ] 표준 크기: `h-10` (기본), `h-9` (작음), `h-8` (아이콘) +- [ ] 텍스트: `text-sm font-medium` +- [ ] 아이콘 + 텍스트: `gap-2` + +### 입력 필드 +- [ ] 높이: `h-10` +- [ ] 텍스트: `text-sm` +- [ ] Label과 Input `htmlFor`/`id` 연결 +- [ ] `space-y-2` 구조 + +### 테이블 +- [ ] `rounded-lg border bg-card shadow-sm` +- [ ] 헤더: `h-12 text-sm font-semibold bg-muted/50` +- [ ] 데이터 행: `h-16 text-sm` +- [ ] Hover: `hover:bg-muted/50` + +### 반응형 +- [ ] 모바일 우선 디자인 +- [ ] `sm:`, `md:`, `lg:` 브레이크포인트 사용 +- [ ] `flex-col sm:flex-row` 패턴 + +### 접근성 +- [ ] Label `htmlFor` 속성 +- [ ] Input `id` 속성 +- [ ] 버튼 `aria-label` +- [ ] Switch `aria-label` + +## 마이그레이션 절차 + +1. **페이지 컴포넌트 수정** (`page.tsx`) + - 레이아웃 구조 변경 + - 색상 CSS Variables로 변경 + - 페이지 헤더 표준화 + +2. **Toolbar 컴포넌트 수정** + - 검색 영역 스타일 통일 + - 버튼 스타일 표준화 + - 반응형 레이아웃 적용 + +3. **Table 컴포넌트 수정** + - 테이블 컨테이너 스타일 통일 + - 헤더/데이터 행 높이 표준화 + - 로딩/Empty State 표준화 + +4. **Form 컴포넌트 수정** (있는 경우) + - 필드 구조 표준화 + - 라벨과 입력 필드 연결 + - 에러 메시지 스타일 통일 + +5. **Modal 컴포넌트 수정** (있는 경우) + - Dialog 표준 패턴 적용 + - 반응형 크기 (`max-w-[95vw] sm:max-w-[500px]`) + - 버튼 스타일 표준화 + +6. **린트 에러 확인** + ```bash + # 수정한 파일들 확인 + npm run lint + ``` + +7. **테스트** + - 기능 동작 확인 + - 반응형 확인 (모바일/태블릿/데스크톱) + - 다크모드 확인 (있는 경우) + +## 참고 파일 + +### 완성된 예시 +- `frontend/app/(main)/admin/userMng/page.tsx` +- `frontend/components/admin/UserToolbar.tsx` +- `frontend/components/admin/UserTable.tsx` +- `frontend/components/admin/UserManagement.tsx` + +### 스타일 가이드 +- `.cursorrules` - 전체 스타일 규칙 +- Section 1-21: 각 스타일 요소별 상세 가이드 + diff --git a/frontend/app/(main)/admin/batchmng/page.tsx b/frontend/app/(main)/admin/batchmng/page.tsx index 184ae578..46aedf1f 100644 --- a/frontend/app/(main)/admin/batchmng/page.tsx +++ b/frontend/app/(main)/admin/batchmng/page.tsx @@ -1,17 +1,8 @@ "use client"; import React, { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@/components/ui/table"; import { Plus, Search, @@ -26,6 +17,7 @@ import { BatchMapping, } from "@/lib/api/batch"; import BatchCard from "@/components/admin/BatchCard"; +import { ScrollToTop } from "@/components/common/ScrollToTop"; export default function BatchManagementPage() { const router = useRouter(); @@ -178,187 +170,198 @@ export default function BatchManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

배치 관리

-

데이터베이스 간 배치 작업을 관리합니다.

+
+
+ {/* 페이지 헤더 */} +
+

배치 관리

+

데이터베이스 간 배치 작업을 관리합니다.

- -
- {/* 검색 및 필터 */} - - -
-
- - handleSearch(e.target.value)} - className="pl-10" - /> + {/* 검색 및 액션 영역 */} +
+ {/* 검색 영역 */} +
+
+
+ + handleSearch(e.target.value)} + className="h-10 pl-10 text-sm" + /> +
+
- - - {/* 배치 목록 */} - - - - 배치 목록 ({batchConfigs.length}개) - {loading && } - - - - {batchConfigs.length === 0 ? ( -
- -

배치가 없습니다

-

- {searchTerm ? "검색 결과가 없습니다." : "새로운 배치를 추가해보세요."} -

+ {/* 액션 버튼 영역 */} +
+
+ 총{" "} + + {batchConfigs.length.toLocaleString()} + {" "} + 건 +
+ +
+
+ + {/* 배치 목록 */} + {batchConfigs.length === 0 ? ( +
+
+ +
+

배치가 없습니다

+

+ {searchTerm ? "검색 결과가 없습니다." : "새로운 배치를 추가해보세요."} +

+
{!searchTerm && ( )}
- ) : ( -
- {batchConfigs.map((batch) => ( - { - console.log("🖱️ 비활성화/활성화 버튼 클릭:", { batchId, currentStatus }); - toggleBatchStatus(batchId, currentStatus); - }} - onEdit={(batchId) => router.push(`/admin/batchmng/edit/${batchId}`)} - onDelete={deleteBatch} - getMappingSummary={getMappingSummary} - /> - ))} -
- )} - - - - {/* 페이지네이션 */} - {totalPages > 1 && ( -
- - -
- {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { - const pageNum = i + 1; - return ( - - ); - })}
- - -
- )} + ) : ( +
+ {batchConfigs.map((batch) => ( + { + toggleBatchStatus(batchId, currentStatus); + }} + onEdit={(batchId) => router.push(`/admin/batchmng/edit/${batchId}`)} + onDelete={deleteBatch} + getMappingSummary={getMappingSummary} + /> + ))} +
+ )} - {/* 배치 타입 선택 모달 */} - {isBatchTypeModalOpen && ( -
- - - 배치 타입 선택 - - -
- {/* DB → DB */} -
handleBatchTypeSelect('db-to-db')} - > -
- - - -
-
-
DB → DB
-
데이터베이스 간 데이터 동기화
-
+ {/* 페이지네이션 */} + {totalPages > 1 && ( +
+ + +
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + const pageNum = i + 1; + return ( + + ); + })} +
+ + +
+ )} + + {/* 배치 타입 선택 모달 */} + {isBatchTypeModalOpen && ( +
+
+
+

배치 타입 선택

+ +
+ {/* DB → DB */} + + + {/* REST API → DB */} +
- {/* REST API → DB */} -
handleBatchTypeSelect('restapi-to-db')} - > -
- - - -
-
-
REST API → DB
-
REST API에서 데이터베이스로 데이터 수집
-
+
+
+
+
+ )} +
-
- -
- - -
- )} + {/* Scroll to Top 버튼 */} +
); } \ No newline at end of file diff --git a/frontend/app/(main)/admin/commonCode/page.tsx b/frontend/app/(main)/admin/commonCode/page.tsx index bdb435f7..9c1bf507 100644 --- a/frontend/app/(main)/admin/commonCode/page.tsx +++ b/frontend/app/(main)/admin/commonCode/page.tsx @@ -1,59 +1,49 @@ "use client"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CodeCategoryPanel } from "@/components/admin/CodeCategoryPanel"; import { CodeDetailPanel } from "@/components/admin/CodeDetailPanel"; import { useSelectedCategory } from "@/hooks/useSelectedCategory"; -// import { useMultiLang } from "@/hooks/useMultiLang"; // 무한 루프 방지를 위해 임시 제거 +import { ScrollToTop } from "@/components/common/ScrollToTop"; export default function CommonCodeManagementPage() { - // const { getText } = useMultiLang(); // 무한 루프 방지를 위해 임시 제거 const { selectedCategoryCode, selectCategory } = useSelectedCategory(); return ( -
-
- {/* 페이지 제목 */} -
-
-

공통코드 관리

-

시스템에서 사용하는 공통코드를 관리합니다

+
+
+ {/* 페이지 헤더 */} +
+

공통코드 관리

+

시스템에서 사용하는 공통코드를 관리합니다

+
+ + {/* 메인 콘텐츠 - 좌우 레이아웃 */} +
+ {/* 좌측: 카테고리 패널 */} +
+
+

코드 카테고리

+ +
+
+ + {/* 우측: 코드 상세 패널 */} +
+
+

+ 코드 상세 정보 + {selectedCategoryCode && ( + ({selectedCategoryCode}) + )} +

+ +
- - {/* 메인 콘텐츠 */} - {/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */} -
- {/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */} -
- - - 📂 코드 카테고리 - - - - - -
- - {/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */} -
- - - - 📋 코드 상세 정보 - {selectedCategoryCode && ( - ({selectedCategoryCode}) - )} - - - - - - -
-
+ + {/* Scroll to Top 버튼 */} +
); } diff --git a/frontend/app/(main)/admin/company/page.tsx b/frontend/app/(main)/admin/company/page.tsx index c24a3e10..c24afc7a 100644 --- a/frontend/app/(main)/admin/company/page.tsx +++ b/frontend/app/(main)/admin/company/page.tsx @@ -1,21 +1,25 @@ import { CompanyManagement } from "@/components/admin/CompanyManagement"; +import { ScrollToTop } from "@/components/common/ScrollToTop"; /** * 회사 관리 페이지 */ export default function CompanyPage() { return ( -
-
- {/* 페이지 제목 */} -
-
-

회사 관리

-

시스템에서 사용하는 회사 정보를 관리합니다

-
+
+
+ {/* 페이지 헤더 */} +
+

회사 관리

+

시스템에서 사용하는 회사 정보를 관리합니다

+ + {/* 메인 컨텐츠 */}
+ + {/* Scroll to Top 버튼 */} +
); } diff --git a/frontend/app/(main)/admin/dashboard/page.tsx b/frontend/app/(main)/admin/dashboard/page.tsx index d1ca6125..d5608e76 100644 --- a/frontend/app/(main)/admin/dashboard/page.tsx +++ b/frontend/app/(main)/admin/dashboard/page.tsx @@ -126,102 +126,108 @@ export default function DashboardListPage() { if (loading) { return ( -
+
-
로딩 중...
-
대시보드 목록을 불러오고 있습니다
+
로딩 중...
+
대시보드 목록을 불러오고 있습니다
); } return ( -
-
- {/* 헤더 */} -
-

대시보드 관리

-

대시보드를 생성하고 관리할 수 있습니다

+
+
+ {/* 페이지 헤더 */} +
+

대시보드 관리

+

대시보드를 생성하고 관리할 수 있습니다

- {/* 액션 바 */} -
-
- + {/* 검색 및 액션 */} +
+
+ setSearchTerm(e.target.value)} - className="pl-9" + className="h-10 pl-10 text-sm" />
-
{/* 에러 메시지 */} {error && ( - -

{error}

-
+
+
+

오류가 발생했습니다

+ +
+

{error}

+
)} {/* 대시보드 목록 */} {dashboards.length === 0 ? ( - -
- +
+
+

대시보드가 없습니다

-

대시보드가 없습니다

-

첫 번째 대시보드를 생성하여 데이터 시각화를 시작하세요

- - +
) : ( - +
- - 제목 - 설명 - 생성일 - 수정일 - 작업 + + 제목 + 설명 + 생성일 + 수정일 + 작업 {dashboards.map((dashboard) => ( - - {dashboard.title} - + + {dashboard.title} + {dashboard.description || "-"} - {formatDate(dashboard.createdAt)} - {formatDate(dashboard.updatedAt)} - + {formatDate(dashboard.createdAt)} + {formatDate(dashboard.updatedAt)} + - router.push(`/admin/dashboard/edit/${dashboard.id}`)} - className="gap-2" + className="gap-2 text-sm" > 편집 - handleCopy(dashboard)} className="gap-2"> + handleCopy(dashboard)} className="gap-2 text-sm"> 복사 handleDeleteClick(dashboard.id, dashboard.title)} - className="gap-2 text-red-600 focus:text-red-600" + className="gap-2 text-sm text-destructive focus:text-destructive" > 삭제 @@ -233,23 +239,27 @@ export default function DashboardListPage() { ))}
- +
)}
{/* 삭제 확인 모달 */} - + - 대시보드 삭제 - + 대시보드 삭제 + "{deleteTarget?.title}" 대시보드를 삭제하시겠습니까? -
이 작업은 되돌릴 수 없습니다. +
+ 이 작업은 되돌릴 수 없습니다.
- - 취소 - + + 취소 + 삭제 @@ -258,16 +268,18 @@ export default function DashboardListPage() { {/* 성공 모달 */} - + -
- +
+
- 완료 - {successMessage} + 완료 + {successMessage}
- +
diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index ff7e5aeb..f8a77e11 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -7,6 +7,7 @@ import { FlowEditor } from "@/components/dataflow/node-editor/FlowEditor"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { ScrollToTop } from "@/components/common/ScrollToTop"; import { ArrowLeft } from "lucide-react"; type Step = "list" | "editor"; @@ -50,17 +51,17 @@ export default function DataFlowPage() { // 에디터 모드일 때는 레이아웃 없이 전체 화면 사용 if (isEditorMode) { return ( -
+
{/* 에디터 헤더 */} -
+
-

노드 플로우 에디터

-

+

노드 플로우 에디터

+

드래그 앤 드롭으로 데이터 제어 플로우를 시각적으로 설계합니다

@@ -76,19 +77,20 @@ export default function DataFlowPage() { } return ( -
-
- {/* 페이지 제목 */} -
-
-

제어 관리

-

노드 기반 데이터 플로우를 시각적으로 설계하고 관리합니다

-
+
+
+ {/* 페이지 헤더 */} +
+

제어 관리

+

노드 기반 데이터 플로우를 시각적으로 설계하고 관리합니다

{/* 플로우 목록 */}
+ + {/* Scroll to Top 버튼 */} +
); } diff --git a/frontend/app/(main)/admin/external-call-configs/page.tsx b/frontend/app/(main)/admin/external-call-configs/page.tsx index 805220ca..7e433ec7 100644 --- a/frontend/app/(main)/admin/external-call-configs/page.tsx +++ b/frontend/app/(main)/admin/external-call-configs/page.tsx @@ -161,205 +161,201 @@ export default function ExternalCallConfigsPage() { }; return ( -
-
- {/* 페이지 헤더 */} -
-
-

외부 호출 관리

-

Discord, Slack, 카카오톡 등 외부 호출 설정을 관리합니다.

+
+
+ {/* 페이지 헤더 */} +
+

외부 호출 관리

+

Discord, Slack, 카카오톡 등 외부 호출 설정을 관리합니다.

- -
- {/* 검색 및 필터 */} - - - - - 검색 및 필터 - - - - {/* 검색 */} -
-
- setSearchQuery(e.target.value)} - onKeyPress={handleSearchKeyPress} - /> + {/* 검색 및 필터 영역 */} +
+ {/* 첫 번째 줄: 검색 + 추가 버튼 */} +
+
+
+
+ + setSearchQuery(e.target.value)} + onKeyPress={handleSearchKeyPress} + className="h-10 pl-10 text-sm" + /> +
+
+
-
- {/* 필터 */} -
-
- - -
+ {/* 두 번째 줄: 필터 */} +
+ -
- - -
+ -
- - -
+
- - +
- {/* 설정 목록 */} - - - 외부 호출 설정 목록 - - + {/* 설정 목록 */} +
{loading ? ( // 로딩 상태 -
-
로딩 중...
+
+
로딩 중...
) : configs.length === 0 ? ( // 빈 상태 -
-
- -

등록된 외부 호출 설정이 없습니다.

-

새 외부 호출을 추가해보세요.

+
+
+

등록된 외부 호출 설정이 없습니다.

+

새 외부 호출을 추가해보세요.

) : ( // 설정 테이블 목록 - - 설정명 - 호출 타입 - API 타입 - 설명 - 상태 - 생성일 - 작업 + + 설정명 + 호출 타입 + API 타입 + 설명 + 상태 + 생성일 + 작업 {configs.map((config) => ( - - {config.config_name} - + + {config.config_name} + {getCallTypeLabel(config.call_type)} - + {config.api_type ? ( {getApiTypeLabel(config.api_type)} ) : ( - - + - )} - +
{config.description ? ( - + {config.description} ) : ( - - + - )}
- + {config.is_active === "Y" ? "활성" : "비활성"} - + {config.created_date ? new Date(config.created_date).toLocaleDateString() : "-"} - +
- -
@@ -368,8 +364,7 @@ export default function ExternalCallConfigsPage() {
)} - - +
{/* 외부 호출 설정 모달 */} - + - 외부 호출 설정 삭제 - + 외부 호출 설정 삭제 + "{configToDelete?.config_name}" 설정을 삭제하시겠습니까?
이 작업은 되돌릴 수 없습니다.
- - 취소 - + + + 취소 + + 삭제 diff --git a/frontend/app/(main)/admin/external-connections/page.tsx b/frontend/app/(main)/admin/external-connections/page.tsx index 42a20bdb..3c80ac58 100644 --- a/frontend/app/(main)/admin/external-connections/page.tsx +++ b/frontend/app/(main)/admin/external-connections/page.tsx @@ -227,14 +227,12 @@ export default function ExternalConnectionsPage() { }; return ( -
-
- {/* 페이지 제목 */} -
-
-

외부 커넥션 관리

-

외부 데이터베이스 및 REST API 연결 정보를 관리합니다

-
+
+
+ {/* 페이지 헤더 */} +
+

외부 커넥션 관리

+

외부 데이터베이스 및 REST API 연결 정보를 관리합니다

{/* 탭 */} @@ -253,166 +251,152 @@ export default function ExternalConnectionsPage() { {/* 데이터베이스 연결 탭 */} {/* 검색 및 필터 */} - - -
-
- {/* 검색 */} -
- - setSearchTerm(e.target.value)} - className="w-64 pl-10" - /> -
- - {/* DB 타입 필터 */} - - - {/* 활성 상태 필터 */} - -
- - {/* 추가 버튼 */} - +
+
+ {/* 검색 */} +
+ + setSearchTerm(e.target.value)} + className="h-10 pl-10 text-sm" + />
- - + + {/* DB 타입 필터 */} + + + {/* 활성 상태 필터 */} + +
+ + {/* 추가 버튼 */} + +
{/* 연결 목록 */} {loading ? ( -
-
로딩 중...
+
+
로딩 중...
) : connections.length === 0 ? ( - - -
- -

등록된 연결이 없습니다

-

새 외부 데이터베이스 연결을 추가해보세요.

- -
-
-
+
+
+

등록된 연결이 없습니다

+
+
) : ( - - - +
+
- - 연결명 - DB 타입 - 호스트:포트 - 데이터베이스 - 사용자 - 상태 - 생성일 - 연결 테스트 - 작업 + + 연결명 + DB 타입 + 호스트:포트 + 데이터베이스 + 사용자 + 상태 + 생성일 + 연결 테스트 + 작업 {connections.map((connection) => ( - - + +
{connection.connection_name}
- - + + {DB_TYPE_LABELS[connection.db_type] || connection.db_type} - + {connection.host}:{connection.port} - {connection.database_name} - {connection.username} - - + {connection.database_name} + {connection.username} + + {connection.is_active === "Y" ? "활성" : "비활성"} - + {connection.created_date ? new Date(connection.created_date).toLocaleDateString() : "N/A"} - +
{testResults.has(connection.id!) && ( - + {testResults.get(connection.id!) ? "성공" : "실패"} )}
- -
+ +
@@ -422,8 +406,7 @@ export default function ExternalConnectionsPage() { ))}
-
-
+
)} {/* 연결 설정 모달 */} @@ -439,20 +422,25 @@ export default function ExternalConnectionsPage() { {/* 삭제 확인 다이얼로그 */} - + - 연결 삭제 확인 - + 연결 삭제 확인 + "{connectionToDelete?.connection_name}" 연결을 삭제하시겠습니까?
- 이 작업은 되돌릴 수 없습니다. + 이 작업은 되돌릴 수 없습니다.
- - 취소 + + + 취소 + 삭제 diff --git a/frontend/app/(main)/admin/flow-management/page.tsx b/frontend/app/(main)/admin/flow-management/page.tsx index f36bd5a2..e8166662 100644 --- a/frontend/app/(main)/admin/flow-management/page.tsx +++ b/frontend/app/(main)/admin/flow-management/page.tsx @@ -9,9 +9,8 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; -import { Plus, Edit2, Trash2, Play, Workflow, Table, Calendar, User, Check, ChevronsUpDown } from "lucide-react"; +import { Plus, Edit2, Trash2, Workflow, Table, Calendar, User, Check, ChevronsUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Dialog, @@ -32,6 +31,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { tableManagementApi } from "@/lib/api/tableManagement"; +import { ScrollToTop } from "@/components/common/ScrollToTop"; export default function FlowManagementPage() { const router = useRouter(); @@ -45,11 +45,15 @@ export default function FlowManagementPage() { const [selectedFlow, setSelectedFlow] = useState(null); // 테이블 목록 관련 상태 - const [tableList, setTableList] = useState([]); // 내부 DB 테이블 + const [tableList, setTableList] = useState>( + [], + ); const [loadingTables, setLoadingTables] = useState(false); const [openTableCombobox, setOpenTableCombobox] = useState(false); const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal"); // "internal" 또는 외부 DB connection ID - const [externalConnections, setExternalConnections] = useState([]); + const [externalConnections, setExternalConnections] = useState< + Array<{ id: number; connection_name: string; db_type: string }> + >([]); const [externalTableList, setExternalTableList] = useState([]); const [loadingExternalTables, setLoadingExternalTables] = useState(false); @@ -74,10 +78,10 @@ export default function FlowManagementPage() { variant: "destructive", }); } - } catch (error: any) { + } catch (error) { toast({ title: "오류 발생", - description: error.message, + description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", variant: "destructive", }); } finally { @@ -87,6 +91,7 @@ export default function FlowManagementPage() { useEffect(() => { loadFlows(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 테이블 목록 로드 (내부 DB) @@ -128,7 +133,8 @@ export default function FlowManagementPage() { if (data.success && data.data) { // 메인 데이터베이스(현재 시스템) 제외 - connection_name에 "메인" 또는 "현재 시스템"이 포함된 것 필터링 const filtered = data.data.filter( - (conn: any) => !conn.connection_name.includes("메인") && !conn.connection_name.includes("현재 시스템"), + (conn: { connection_name: string }) => + !conn.connection_name.includes("메인") && !conn.connection_name.includes("현재 시스템"), ); setExternalConnections(filtered); } @@ -164,7 +170,9 @@ export default function FlowManagementPage() { if (data.success && data.data) { const tables = Array.isArray(data.data) ? data.data : []; const tableNames = tables - .map((t: any) => (typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name)) + .map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) => + typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name, + ) .filter(Boolean); setExternalTableList(tableNames); } else { @@ -224,10 +232,10 @@ export default function FlowManagementPage() { variant: "destructive", }); } - } catch (error: any) { + } catch (error) { toast({ title: "오류 발생", - description: error.message, + description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", variant: "destructive", }); } @@ -254,10 +262,10 @@ export default function FlowManagementPage() { variant: "destructive", }); } - } catch (error: any) { + } catch (error) { toast({ title: "오류 발생", - description: error.message, + description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", variant: "destructive", }); } @@ -269,317 +277,342 @@ export default function FlowManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

- - 플로우 관리 -

-

업무 프로세스 플로우를 생성하고 관리합니다

+
+
+ {/* 페이지 헤더 */} +
+

플로우 관리

+

업무 프로세스 플로우를 생성하고 관리합니다

- -
- {/* 플로우 카드 목록 */} - {loading ? ( -
-

로딩 중...

+ {/* 액션 버튼 영역 */} +
+
- ) : flows.length === 0 ? ( - - - -

생성된 플로우가 없습니다

- -
-
- ) : ( -
- {flows.map((flow) => ( - handleEdit(flow.id)} - > - -
+ + {/* 플로우 카드 목록 */} + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, index) => ( +
+
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+ ))} +
+
+
+
+
+
+ ))} +
+ ) : flows.length === 0 ? ( +
+
+
+ +
+

생성된 플로우가 없습니다

+

+ 새 플로우를 생성하여 업무 프로세스를 관리해보세요. +

+ +
+
+ ) : ( +
+ {flows.map((flow) => ( +
handleEdit(flow.id)} + > + {/* 헤더 */} +
- - {flow.name} +
+

{flow.name}

{flow.isActive && ( - - 활성 - + 활성 )} - - - {flow.description || "설명 없음"} - -
-
- - -
-
- - {flow.tableName} - -
- - 생성자: {flow.createdBy} -
-
- - {new Date(flow.updatedAt).toLocaleDateString("ko-KR")} +
+

{flow.description || "설명 없음"}

-
+ {/* 정보 */} +
+
+
+ {flow.tableName} + +
+ + 생성자: {flow.createdBy} +
+
+ + + {new Date(flow.updatedAt).toLocaleDateString("ko-KR")} + +
+ + + {/* 액션 */} +
- - - ))} - - )} - - {/* 생성 다이얼로그 */} - - - - 새 플로우 생성 - - 새로운 업무 프로세스 플로우를 생성합니다 - - - -
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="예: 제품 수명주기 관리" - className="h-8 text-xs sm:h-10 sm:text-sm" - /> -
- - {/* DB 소스 선택 */} -
- - -

- 플로우에서 사용할 데이터베이스를 선택합니다 -

-
- - {/* 테이블 선택 */} -
- - - - - - - - - - 테이블을 찾을 수 없습니다. - - {selectedDbSource === "internal" - ? // 내부 DB 테이블 목록 - tableList.map((table) => ( - { - console.log("📝 Internal table selected:", { - tableName: table.tableName, - currentValue, - }); - setFormData({ ...formData, tableName: currentValue }); - setOpenTableCombobox(false); - }} - className="text-xs sm:text-sm" - > - -
- {table.displayName || table.tableName} - {table.description && ( - {table.description} - )} -
-
- )) - : // 외부 DB 테이블 목록 - externalTableList.map((tableName, index) => ( - { - setFormData({ ...formData, tableName: currentValue }); - setOpenTableCombobox(false); - }} - className="text-xs sm:text-sm" - > - -
{tableName}
-
- ))} -
-
-
-
-
-

- 플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다) -

-
- -
- -