+```
+
+**표준 색상 토큰:**
+
+- `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
+// 페이지 제목
+
+
+// 섹션 제목
+
+
+
+
+// 본문 텍스트
+
+
+// 보조 텍스트
+
+
+
+// 라벨
+
+```
+
+## 4. Spacing System (간격)
+
+### 일관된 간격 (4px 기준)
+
+```tsx
+// 페이지 레벨 간격
+ // 24px
+
+// 섹션 레벨 간격
+
// 16px
+
+// 필드 레벨 간격
+
// 8px
+
+// 패딩
+
// 24px (카드)
+
// 16px (내부 섹션)
+
+// 갭
+
// 16px (flex/grid)
+
// 8px (버튼 그룹)
+```
+
+## 5. 검색 툴바 (Toolbar)
+
+### 패턴 A: 통합 검색 영역 (권장)
+
+```tsx
+
+ {/* 검색 및 액션 영역 */}
+
+ {/* 검색 영역 */}
+
+ {/* 통합 검색 */}
+
+
+ {/* 고급 검색 토글 */}
+
+ 고급 검색
+
+
+
+ {/* 액션 버튼 영역 */}
+
+
+ 총{" "}
+
+ {count.toLocaleString()}
+ {" "}
+ 건
+
+
+
+ 등록
+
+
+
+
+ {/* 고급 검색 옵션 */}
+ {showAdvanced && (
+
+ )}
+
+```
+
+### 패턴 B: 제목 + 검색 + 버튼 한 줄 (공간 효율적)
+
+```tsx
+{
+ /* 상단 헤더: 제목 + 검색 + 버튼 */
+}
+
+ {/* 왼쪽: 제목 */}
+
페이지 제목
+
+ {/* 오른쪽: 검색 + 버튼 */}
+
+ {/* 필터 선택 */}
+
+
+
+
+
+
+
+
+ {/* 검색 입력 */}
+
+
+
+
+ {/* 초기화 버튼 */}
+
+ 초기화
+
+
+ {/* 주요 액션 버튼 */}
+
+
+ 등록
+
+
+ {/* 조건부 버튼 (선택 시) */}
+ {selectedCount > 0 && (
+
+ 삭제 ({selectedCount})
+
+ )}
+
+
;
+```
+
+**필수 적용 사항:**
+
+- ❌ 검색 영역에 박스/테두리 사용 금지
+- ✅ 검색창 권장 너비: `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
+{
+ /* 드롭다운 컨테이너 */
+}
+
+
+ {/* 트리거 버튼 */}
+
setIsOpen(!isOpen)}
+ className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
+ >
+
+ {value || "선택하세요"}
+
+
+
+
+
+
+ {/* 드롭다운 메뉴 */}
+ {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
+// 카드/패널
+
+
+// 입력 필드
+
+
+// 버튼
+
+```
+
+### 5. Button Variants (버튼 스타일)
+
+**표준 variants 사용**
+```tsx
+// Primary 액션
+
+
+ 사용자 등록
+
+
+// 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
+
+```
+
+### 12. Responsive Design (반응형)
+
+**모바일 우선 접근**
+```tsx
+// 레이아웃
+
+
+// 그리드
+
+
+// 텍스트
+
+
+// 간격
+
+```
+
+### 13. Accessibility (접근성)
+
+**필수 적용 사항**
+```tsx
+// Label과 Input 연결
+
+ 사용자 ID
+
+
+
+// 버튼에 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"
- />
+ {/* 검색 및 액션 영역 */}
+
+ {/* 검색 영역 */}
+
-
-
- {/* 배치 목록 */}
-
-
-
- 배치 목록 ({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 && (
-
-
setCurrentPage(prev => Math.max(1, prev - 1))}
- disabled={currentPage === 1}
- >
- 이전
-
-
-
- {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
- const pageNum = i + 1;
- return (
- setCurrentPage(pageNum)}
- >
- {pageNum}
-
- );
- })}
-
-
setCurrentPage(prev => Math.min(totalPages, prev + 1))}
- disabled={currentPage === totalPages}
- >
- 다음
-
-
- )}
+ ) : (
+
+ {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 && (
+
+
setCurrentPage(prev => Math.max(1, prev - 1))}
+ disabled={currentPage === 1}
+ className="h-10 text-sm font-medium"
+ >
+ 이전
+
+
+
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
+ const pageNum = i + 1;
+ return (
+ setCurrentPage(pageNum)}
+ className="h-10 min-w-[40px] text-sm"
+ >
+ {pageNum}
+
+ );
+ })}
+
+
+
setCurrentPage(prev => Math.min(totalPages, prev + 1))}
+ disabled={currentPage === totalPages}
+ className="h-10 text-sm font-medium"
+ >
+ 다음
+
+
+ )}
+
+ {/* 배치 타입 선택 모달 */}
+ {isBatchTypeModalOpen && (
+
+
+
+
배치 타입 선택
+
+
+ {/* DB → DB */}
+
handleBatchTypeSelect('db-to-db')}
+ >
+
+
+ →
+
+
+
+
DB → DB
+
데이터베이스 간 데이터 동기화
+
+
+
+ {/* REST API → DB */}
+
handleBatchTypeSelect('restapi-to-db')}
+ >
+
+ 🌐
+ →
+
+
+
+
REST API → DB
+
REST API에서 데이터베이스로 데이터 수집
+
+
- {/* REST API → DB */}
-
handleBatchTypeSelect('restapi-to-db')}
- >
-
-
-
REST API → DB
-
REST API에서 데이터베이스로 데이터 수집
-
+
+ setIsBatchTypeModalOpen(false)}
+ className="h-10 text-sm font-medium"
+ >
+ 취소
+
+
+
+ )}
+
-
- setIsBatchTypeModalOpen(false)}
- >
- 취소
-
-
-
-
-
- )}
+ {/* 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"
/>
-
router.push("/admin/dashboard/new")} className="gap-2">
- 새 대시보드 생성
+ router.push("/admin/dashboard/new")} className="h-10 gap-2 text-sm font-medium">
+
+ 새 대시보드 생성
{/* 에러 메시지 */}
{error && (
-
- {error}
-
+
+
+
오류가 발생했습니다
+
setError(null)}
+ className="text-destructive transition-colors hover:text-destructive/80"
+ aria-label="에러 메시지 닫기"
+ >
+ ✕
+
+
+
{error}
+
)}
{/* 대시보드 목록 */}
{dashboards.length === 0 ? (
-
-
-
+
+
-
대시보드가 없습니다
-
첫 번째 대시보드를 생성하여 데이터 시각화를 시작하세요
-
router.push("/admin/dashboard/new")} className="gap-2">
- 새 대시보드 생성
-
-
+
) : (
-
+
-
- 제목
- 설명
- 생성일
- 수정일
- 작업
+
+ 제목
+ 설명
+ 생성일
+ 수정일
+ 작업
{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}
- setSuccessDialogOpen(false)}>확인
+ setSuccessDialogOpen(false)} className="h-8 text-xs sm:h-10 sm:text-sm">
+ 확인
+
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"
+ />
+
+
+
+
+ 검색
+
-
-
+
+
+ 새 외부 호출 추가
- {/* 필터 */}
-
-
- 호출 타입
-
- setFilter((prev) => ({
- ...prev,
- call_type: value === "all" ? undefined : value,
- }))
- }
- >
-
-
-
-
- 전체
- {CALL_TYPE_OPTIONS.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
+ {/* 두 번째 줄: 필터 */}
+
+
+ setFilter((prev) => ({
+ ...prev,
+ call_type: value === "all" ? undefined : value,
+ }))
+ }
+ >
+
+
+
+
+ 전체
+ {CALL_TYPE_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
-
- API 타입
-
- setFilter((prev) => ({
- ...prev,
- api_type: value === "all" ? undefined : value,
- }))
- }
- >
-
-
-
-
- 전체
- {API_TYPE_OPTIONS.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
+
+ setFilter((prev) => ({
+ ...prev,
+ api_type: value === "all" ? undefined : value,
+ }))
+ }
+ >
+
+
+
+
+ 전체
+ {API_TYPE_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
-
- 상태
-
- setFilter((prev) => ({
- ...prev,
- is_active: value,
- }))
- }
- >
-
-
-
-
- {ACTIVE_STATUS_OPTIONS.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
+
+ setFilter((prev) => ({
+ ...prev,
+ is_active: value,
+ }))
+ }
+ >
+
+
+
+
+ {ACTIVE_STATUS_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
-
-
+
- {/* 설정 목록 */}
-
-
- 외부 호출 설정 목록
-
-
+ {/* 설정 목록 */}
+
{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() : "-"}
-
+
- handleTestConfig(config)} title="테스트">
-
+ handleTestConfig(config)}
+ title="테스트"
+ >
+
- handleEditConfig(config)} title="편집">
-
+ handleEditConfig(config)}
+ title="편집"
+ >
+
handleDeleteConfig(config)}
- className="text-destructive hover:text-destructive"
title="삭제"
>
-
+
@@ -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 타입 필터 */}
-
-
-
-
-
- {supportedDbTypes.map((type) => (
-
- {type.label}
-
- ))}
-
-
-
- {/* 활성 상태 필터 */}
-
-
-
-
-
- {ACTIVE_STATUS_OPTIONS.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
-
- {/* 추가 버튼 */}
-
- 새 연결 추가
-
+
+
+ {/* 검색 */}
+
+
+ setSearchTerm(e.target.value)}
+ className="h-10 pl-10 text-sm"
+ />
-
-
+
+ {/* DB 타입 필터 */}
+
+
+
+
+
+ {supportedDbTypes.map((type) => (
+
+ {type.label}
+
+ ))}
+
+
+
+ {/* 활성 상태 필터 */}
+
+
+
+
+
+ {ACTIVE_STATUS_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+ {/* 추가 버튼 */}
+
+
+ 새 연결 추가
+
+
{/* 연결 목록 */}
{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"}
-
+
handleTestConnection(connection)}
disabled={testingConnections.has(connection.id!)}
- className="h-7 px-2 text-xs"
+ className="h-9 text-sm"
>
{testingConnections.has(connection.id!) ? "테스트 중..." : "테스트"}
{testResults.has(connection.id!) && (
-
+
{testResults.get(connection.id!) ? "성공" : "실패"}
)}
-
-
+
+
{
console.log("SQL 쿼리 실행 버튼 클릭 - connection:", connection);
setSelectedConnection(connection);
setSqlModalOpen(true);
}}
- className="h-8 w-8 p-0"
+ className="h-8 w-8"
title="SQL 쿼리 실행"
>
handleEditConnection(connection)}
- className="h-8 w-8 p-0"
+ className="h-8 w-8"
>
handleDeleteConnection(connection)}
- className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
+ className="h-8 w-8 text-destructive hover:bg-destructive/10"
>
@@ -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 (
-
- {/* 헤더 */}
-
-
-
-
- 플로우 관리
-
-
업무 프로세스 플로우를 생성하고 관리합니다
+
+
+ {/* 페이지 헤더 */}
+
+
플로우 관리
+
업무 프로세스 플로우를 생성하고 관리합니다
-
setIsCreateDialogOpen(true)} className="w-full sm:w-auto">
-
- 새 플로우 생성
- 생성
-
-
- {/* 플로우 카드 목록 */}
- {loading ? (
-
-
로딩 중...
+ {/* 액션 버튼 영역 */}
+
+
setIsCreateDialogOpen(true)} className="h-10 gap-2 text-sm font-medium">
+ 새 플로우 생성
+
- ) : flows.length === 0 ? (
-
-
-
- 생성된 플로우가 없습니다
- setIsCreateDialogOpen(true)} className="w-full sm:w-auto">
- 첫 플로우 만들기
-
-
-
- ) : (
-
- {flows.map((flow) => (
-
handleEdit(flow.id)}
- >
-
-
+
+ {/* 플로우 카드 목록 */}
+ {loading ? (
+
+ {Array.from({ length: 6 }).map((_, index) => (
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+
+ ))}
+
+ ) : flows.length === 0 ? (
+
+
+
+
+
+
생성된 플로우가 없습니다
+
+ 새 플로우를 생성하여 업무 프로세스를 관리해보세요.
+
+
setIsCreateDialogOpen(true)} className="mt-4 h-10 gap-2 text-sm font-medium">
+ 첫 플로우 만들기
+
+
+
+ ) : (
+
+ {flows.map((flow) => (
+
handleEdit(flow.id)}
+ >
+ {/* 헤더 */}
+
-
- {flow.name}
+
+
{flow.name}
{flow.isActive && (
-
- 활성
-
+ 활성
)}
-
-
- {flow.description || "설명 없음"}
-
-
-
-
-
-
-
-
-
- 생성자: {flow.createdBy}
-
-
-
- {new Date(flow.updatedAt).toLocaleDateString("ko-KR")}
+
+
{flow.description || "설명 없음"}
-
+ {/* 정보 */}
+
+
+
+
+ 생성자: {flow.createdBy}
+
+
+
+
+ {new Date(flow.updatedAt).toLocaleDateString("ko-KR")}
+
+
+
+
+ {/* 액션 */}
+
{
e.stopPropagation();
handleEdit(flow.id);
}}
>
-
+
편집
{
e.stopPropagation();
setSelectedFlow(flow);
setIsDeleteDialogOpen(true);
}}
>
-
+
-
-
- ))}
-
- )}
-
- {/* 생성 다이얼로그 */}
-
-
-
- 새 플로우 생성
-
- 새로운 업무 프로세스 플로우를 생성합니다
-
-
-
-
-
-
- 플로우 이름 *
-
- setFormData({ ...formData, name: e.target.value })}
- placeholder="예: 제품 수명주기 관리"
- className="h-8 text-xs sm:h-10 sm:text-sm"
- />
-
-
- {/* DB 소스 선택 */}
-
-
데이터베이스 소스
-
{
- const dbSource = value === "internal" ? "internal" : parseInt(value);
- setSelectedDbSource(dbSource);
- // DB 소스 변경 시 테이블 선택 초기화
- setFormData({ ...formData, tableName: "" });
- }}
- >
-
-
-
-
- 내부 데이터베이스
- {externalConnections.map((conn: any) => (
-
- {conn.connection_name} ({conn.db_type?.toUpperCase()})
-
- ))}
-
-
-
- 플로우에서 사용할 데이터베이스를 선택합니다
-
-
-
- {/* 테이블 선택 */}
-
-
- 연결 테이블 *
-
-
-
-
- {formData.tableName
- ? selectedDbSource === "internal"
- ? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
- formData.tableName
- : formData.tableName
- : loadingTables || loadingExternalTables
- ? "로딩 중..."
- : "테이블 선택"}
-
-
-
-
-
-
-
- 테이블을 찾을 수 없습니다.
-
- {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}
-
- ))}
-
-
-
-
-
-
- 플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)
-
-
-
-
-
- 설명
-
-
+
+ ))}
+ )}
-
- setIsCreateDialogOpen(false)}
- className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
- >
- 취소
-
-
- 생성
-
-
-
-
+ {/* 생성 다이얼로그 */}
+
+
+
+ 새 플로우 생성
+
+ 새로운 업무 프로세스 플로우를 생성합니다
+
+
- {/* 삭제 확인 다이얼로그 */}
-
-
-
- 플로우 삭제
-
- 정말로 "{selectedFlow?.name}" 플로우를 삭제하시겠습니까?
- 이 작업은 되돌릴 수 없습니다.
-
-
+
+
+
+ 플로우 이름 *
+
+ setFormData({ ...formData, name: e.target.value })}
+ placeholder="예: 제품 수명주기 관리"
+ className="h-8 text-xs sm:h-10 sm:text-sm"
+ />
+
-
- {
- setIsDeleteDialogOpen(false);
- setSelectedFlow(null);
- }}
- className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
- >
- 취소
-
-
- 삭제
-
-
-
-
+ {/* DB 소스 선택 */}
+
+
데이터베이스 소스
+
{
+ const dbSource = value === "internal" ? "internal" : parseInt(value);
+ setSelectedDbSource(dbSource);
+ // DB 소스 변경 시 테이블 선택 초기화
+ setFormData({ ...formData, tableName: "" });
+ }}
+ >
+
+
+
+
+ 내부 데이터베이스
+ {externalConnections.map((conn) => (
+
+ {conn.connection_name} ({conn.db_type?.toUpperCase()})
+
+ ))}
+
+
+
+ 플로우에서 사용할 데이터베이스를 선택합니다
+
+
+
+ {/* 테이블 선택 */}
+
+
+ 연결 테이블 *
+
+
+
+
+ {formData.tableName
+ ? selectedDbSource === "internal"
+ ? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
+ formData.tableName
+ : formData.tableName
+ : loadingTables || loadingExternalTables
+ ? "로딩 중..."
+ : "테이블 선택"}
+
+
+
+
+
+
+
+ 테이블을 찾을 수 없습니다.
+
+ {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}
+
+ ))}
+
+
+
+
+
+
+ 플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)
+
+
+
+
+
+ 설명
+
+
+
+
+
+ setIsCreateDialogOpen(false)}
+ className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
+ >
+ 취소
+
+
+ 생성
+
+
+
+
+
+ {/* 삭제 확인 다이얼로그 */}
+
+
+
+ 플로우 삭제
+
+ 정말로 “{selectedFlow?.name}” 플로우를 삭제하시겠습니까?
+ 이 작업은 되돌릴 수 없습니다.
+
+
+
+
+ {
+ setIsDeleteDialogOpen(false);
+ setSelectedFlow(null);
+ }}
+ className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
+ >
+ 취소
+
+
+ 삭제
+
+
+
+
+
+
+ {/* Scroll to Top 버튼 */}
+
);
}
diff --git a/frontend/app/(main)/admin/menu/page.tsx b/frontend/app/(main)/admin/menu/page.tsx
index 9c2c17c8..4e9ff1d4 100644
--- a/frontend/app/(main)/admin/menu/page.tsx
+++ b/frontend/app/(main)/admin/menu/page.tsx
@@ -1,20 +1,24 @@
"use client";
import { MenuManagement } from "@/components/admin/MenuManagement";
+import { ScrollToTop } from "@/components/common/ScrollToTop";
export default function MenuPage() {
return (
-
-
- {/* 페이지 제목 */}
-
-
-
메뉴 관리
-
시스템 메뉴를 관리하고 화면을 할당합니다
-
+
+
+ {/* 페이지 헤더 */}
+
+
메뉴 관리
+
시스템 메뉴를 관리하고 화면을 할당합니다
+
+ {/* 메인 컨텐츠 */}
+
+ {/* Scroll to Top 버튼 (모바일/태블릿 전용) */}
+
);
}
diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx
index ae622128..3145d9d3 100644
--- a/frontend/app/(main)/admin/screenMng/page.tsx
+++ b/frontend/app/(main)/admin/screenMng/page.tsx
@@ -2,11 +2,11 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Plus, ArrowLeft, ArrowRight, Circle } from "lucide-react";
+import { ArrowLeft } from "lucide-react";
import ScreenList from "@/components/screen/ScreenList";
import ScreenDesigner from "@/components/screen/ScreenDesigner";
import TemplateManager from "@/components/screen/TemplateManager";
+import { ScrollToTop } from "@/components/common/ScrollToTop";
import { ScreenDefinition } from "@/types/screen";
// 단계별 진행을 위한 타입 정의
@@ -25,17 +25,14 @@ export default function ScreenManagementPage() {
list: {
title: "화면 목록 관리",
description: "생성된 화면들을 확인하고 관리하세요",
- icon: "📋",
},
design: {
title: "화면 설계",
description: "드래그앤드롭으로 화면을 설계하세요",
- icon: "🎨",
},
template: {
title: "템플릿 관리",
description: "화면 템플릿을 관리하고 재사용하세요",
- icon: "📝",
},
};
@@ -65,62 +62,56 @@ export default function ScreenManagementPage() {
}
};
- // 현재 단계가 마지막 단계인지 확인
- const isLastStep = currentStep === "template";
-
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용 (고정 높이)
if (isDesignMode) {
return (
-
+
goToStep("list")} />
);
}
return (
-
-
- {/* 페이지 제목 */}
-
-
-
화면 관리
-
화면을 설계하고 템플릿을 관리합니다
-
+
+
+ {/* 페이지 헤더 */}
+
+
화면 관리
+
화면을 설계하고 템플릿을 관리합니다
{/* 단계별 내용 */}
-
+
{/* 화면 목록 단계 */}
{currentStep === "list" && (
-
-
-
{stepConfig.list.title}
-
goToNextStep("design")}>
- 화면 설계하기
-
-
-
{
- setSelectedScreen(screen);
- goToNextStep("design");
- }}
- />
-
+
{
+ setSelectedScreen(screen);
+ goToNextStep("design");
+ }}
+ />
)}
{/* 템플릿 관리 단계 */}
{currentStep === "template" && (
-
-
-
{stepConfig.template.title}
+
+
+
{stepConfig.template.title}
-
-
+
+
이전 단계
- goToStep("list")}>
+ goToStep("list")}
+ className="h-10 gap-2 text-sm font-medium"
+ >
목록으로 돌아가기
@@ -130,6 +121,9 @@ export default function ScreenManagementPage() {
)}
+
+ {/* Scroll to Top 버튼 */}
+
);
}
diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx
index e415fec8..f3fb6be6 100644
--- a/frontend/app/(main)/admin/tableMng/page.tsx
+++ b/frontend/app/(main)/admin/tableMng/page.tsx
@@ -1,13 +1,11 @@
"use client";
import { useState, useEffect, useMemo, useCallback } from "react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Search, Database, RefreshCw, Settings, Menu, X, Plus, Activity } from "lucide-react";
+import { Search, Database, RefreshCw, Settings, Plus, Activity } from "lucide-react";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { toast } from "sonner";
import { useMultiLang } from "@/hooks/useMultiLang";
@@ -20,7 +18,7 @@ import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin";
import { CreateTableModal } from "@/components/admin/CreateTableModal";
import { AddColumnModal } from "@/components/admin/AddColumnModal";
import { DDLLogViewer } from "@/components/admin/DDLLogViewer";
-// 가상화 스크롤링을 위한 간단한 구현
+import { ScrollToTop } from "@/components/common/ScrollToTop";
interface TableInfo {
tableName: string;
@@ -541,439 +539,440 @@ export default function TableManagementPage() {
}, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]);
return (
-
- {/* 페이지 제목 */}
-
-
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
-
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_DESCRIPTION, "데이터베이스 테이블과 컬럼의 타입을 관리합니다")}
-
- {isSuperAdmin && (
-
- 🔧 최고 관리자 권한으로 새 테이블 생성 및 컬럼 추가가 가능합니다
-
- )}
-
+
+
+ {/* 페이지 헤더 */}
+
+
+
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
+
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_DESCRIPTION, "데이터베이스 테이블과 컬럼의 타입을 관리합니다")}
+
+ {isSuperAdmin && (
+
+ 최고 관리자 권한으로 새 테이블 생성 및 컬럼 추가가 가능합니다
+
+ )}
+
-
- {/* DDL 기능 버튼들 (최고 관리자만) */}
- {isSuperAdmin && (
- <>
-
setCreateTableModalOpen(true)}
- className="bg-green-600 text-white hover:bg-green-700"
- size="sm"
- >
- 새 테이블 생성
-
+
+ {/* DDL 기능 버튼들 (최고 관리자만) */}
+ {isSuperAdmin && (
+ <>
+
setCreateTableModalOpen(true)}
+ className="h-10 gap-2 text-sm font-medium"
+ size="default"
+ >
+ 새 테이블 생성
+
- {selectedTable && (
-
setAddColumnModalOpen(true)} variant="outline" size="sm">
-
- 컬럼 추가
-
+ {selectedTable && (
+
setAddColumnModalOpen(true)} variant="outline" className="h-10 gap-2 text-sm font-medium">
+
+ 컬럼 추가
+
+ )}
+
+
setDdlLogViewerOpen(true)} variant="outline" className="h-10 gap-2 text-sm font-medium">
+
+ DDL 로그
+
+ >
)}
-
setDdlLogViewerOpen(true)} variant="outline" size="sm">
-
- DDL 로그
+
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_REFRESH, "새로고침")}
- >
- )}
-
-
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_REFRESH, "새로고침")}
-
+
+
-
-
- {/* 테이블 목록 */}
-
-
-
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
-
-
-
- {/* 검색 */}
-
+
+ {/* 좌측 사이드바: 테이블 목록 (20%) */}
+
+
+
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
+
+
+ {/* 검색 */}
-
+
setSearchTerm(e.target.value)}
- className="pl-10"
+ className="h-10 pl-10 text-sm"
/>
-
- {/* 테이블 목록 */}
-
- {loading ? (
-
-
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_TABLES, "테이블 로딩 중...")}
-
-
- ) : tables.length === 0 ? (
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
+ {/* 테이블 목록 */}
+
+ {loading ? (
+
+
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_TABLES, "테이블 로딩 중...")}
+
+
+ ) : tables.length === 0 ? (
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
+
+ ) : (
+ tables
+ .filter(
+ (table) =>
+ table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
+ )
+ .map((table) => (
+
handleTableSelect(table.tableName)}
+ >
+
{table.displayName || table.tableName}
+
+ {table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
+
+
+ 컬럼
+
+ {table.columnCount}
+
+
+
+ ))
+ )}
+
+
+
+
+ {/* 우측 메인 영역: 컬럼 타입 관리 (80%) */}
+
+
+
+
+ {selectedTable ? <>테이블 설정 - {selectedTable}> : "테이블 타입 관리"}
+
+
+
+ {!selectedTable ? (
+
+
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
+
+
) : (
- tables
- .filter(
- (table) =>
- table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
- (table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
- )
- .map((table) => (
-
handleTableSelect(table.tableName)}
- >
-
-
-
{table.displayName || table.tableName}
-
- {table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
-
-
-
- {table.columnCount} {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_COLUMN_COUNT, "컬럼")}
-
-
-
- ))
- )}
-
-
-
-
- {/* 컬럼 타입 관리 */}
-
-
-
-
- {selectedTable ? <>테이블 설정 - {selectedTable}> : "테이블 타입 관리"}
-
-
-
- {!selectedTable ? (
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
-
- ) : (
- <>
- {/* 테이블 라벨 설정 */}
-
-
테이블 정보
-
-
- 테이블명 (읽기 전용)
-
-
-
-
표시명
+ <>
+ {/* 테이블 라벨 설정 */}
+
+
setTableLabel(e.target.value)}
- placeholder="테이블 표시명을 입력하세요"
+ placeholder="테이블 표시명"
+ className="h-10 text-sm"
/>
-
-
설명
+
setTableDescription(e.target.value)}
- placeholder="테이블 설명을 입력하세요"
+ placeholder="테이블 설명"
+ className="h-10 text-sm"
/>
-
- {columnsLoading ? (
-
-
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
-
-
- ) : columns.length === 0 ? (
-
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
-
- ) : (
-
- {/* 컬럼 헤더 */}
-
-
컬럼명
-
라벨
-
입력 타입
-
- 상세 설정
-
-
설명
+ {columnsLoading ? (
+
+
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
+
+ ) : columns.length === 0 ? (
+
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
+
+ ) : (
+
+ {/* 컬럼 헤더 */}
+
+
컬럼명
+
라벨
+
입력 타입
+
+ 상세 설정
+
+
설명
+
- {/* 컬럼 리스트 */}
-
{
- const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
- // 스크롤이 끝에 가까워지면 더 많은 데이터 로드
- if (scrollHeight - scrollTop <= clientHeight + 100) {
- loadMoreColumns();
- }
- }}
- >
- {columns.map((column, index) => (
-
-
-
- handleLabelChange(column.columnName, e.target.value)}
- placeholder={column.columnName}
- className="h-7 text-xs"
- />
-
-
- handleInputTypeChange(column.columnName, value)}
- >
-
-
-
-
- {memoizedInputTypeOptions.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
-
- {/* 웹 타입이 'code'인 경우 공통코드 선택 */}
- {column.inputType === "code" && (
+ {/* 컬럼 리스트 */}
+
{
+ const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
+ // 스크롤이 끝에 가까워지면 더 많은 데이터 로드
+ if (scrollHeight - scrollTop <= clientHeight + 100) {
+ loadMoreColumns();
+ }
+ }}
+ >
+ {columns.map((column, index) => (
+
+
+
+ handleLabelChange(column.columnName, e.target.value)}
+ placeholder={column.columnName}
+ className="h-8 text-xs"
+ />
+
+
handleDetailSettingsChange(column.columnName, "code", value)}
+ value={column.inputType || "text"}
+ onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
>
-
-
+
+
- {commonCodeOptions.map((option, index) => (
-
+ {memoizedInputTypeOptions.map((option) => (
+
{option.label}
))}
- )}
- {/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
- {column.inputType === "entity" && (
-
- {/* 🎯 Entity 타입 설정 - 가로 배치 */}
-
-
- Entity 설정
-
-
-
- {/* 참조 테이블 */}
-
-
참조 테이블
-
- handleDetailSettingsChange(column.columnName, "entity", value)
- }
- >
-
-
-
-
- {referenceTableOptions.map((option, index) => (
-
-
- {option.label}
- {option.value}
-
-
- ))}
-
-
+
+
+ {/* 웹 타입이 'code'인 경우 공통코드 선택 */}
+ {column.inputType === "code" && (
+
handleDetailSettingsChange(column.columnName, "code", value)}
+ >
+
+
+
+
+ {commonCodeOptions.map((option, index) => (
+
+ {option.label}
+
+ ))}
+
+
+ )}
+ {/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
+ {column.inputType === "entity" && (
+
+ {/* Entity 타입 설정 - 가로 배치 */}
+
+
+ Entity 설정
- {/* 조인 컬럼 */}
- {column.referenceTable && column.referenceTable !== "none" && (
+
+ {/* 참조 테이블 */}
-
조인 컬럼
+
참조 테이블
- handleDetailSettingsChange(
- column.columnName,
- "entity_reference_column",
- value,
- )
+ handleDetailSettingsChange(column.columnName, "entity", value)
}
>
-
+
- -- 선택 안함 --
- {referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
-
- {refCol.columnName}
-
- ))}
- {(!referenceTableColumns[column.referenceTable] ||
- referenceTableColumns[column.referenceTable].length === 0) && (
-
-
-
- 로딩중
+ {referenceTableOptions.map((option, index) => (
+
+
+ {option.label}
+ {option.value}
- )}
+ ))}
- )}
+
+ {/* 조인 컬럼 */}
+ {column.referenceTable && column.referenceTable !== "none" && (
+
+
조인 컬럼
+
+ handleDetailSettingsChange(
+ column.columnName,
+ "entity_reference_column",
+ value,
+ )
+ }
+ >
+
+
+
+
+ -- 선택 안함 --
+ {referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
+
+ {refCol.columnName}
+
+ ))}
+ {(!referenceTableColumns[column.referenceTable] ||
+ referenceTableColumns[column.referenceTable].length === 0) && (
+
+
+
+ )}
+
+
+
+ )}
+
+
+ {/* 설정 완료 표시 - 간소화 */}
+ {column.referenceTable &&
+ column.referenceTable !== "none" &&
+ column.referenceColumn &&
+ column.referenceColumn !== "none" &&
+ column.displayColumn &&
+ column.displayColumn !== "none" && (
+
+ ✓
+
+ {column.columnName} → {column.referenceTable}.{column.displayColumn}
+
+
+ )}
-
- {/* 설정 완료 표시 - 간소화 */}
- {column.referenceTable &&
- column.referenceTable !== "none" &&
- column.referenceColumn &&
- column.referenceColumn !== "none" &&
- column.displayColumn &&
- column.displayColumn !== "none" && (
-
- ✓
-
- {column.columnName} → {column.referenceTable}.{column.displayColumn}
-
-
- )}
-
- )}
- {/* 다른 웹 타입인 경우 빈 공간 */}
- {column.inputType !== "code" && column.inputType !== "entity" && (
-
-
- )}
+ )}
+ {/* 다른 웹 타입인 경우 빈 공간 */}
+ {column.inputType !== "code" && column.inputType !== "entity" && (
+
-
+ )}
+
+
+ handleColumnChange(index, "description", e.target.value)}
+ placeholder="설명"
+ className="h-8 text-xs"
+ />
+
-
- handleColumnChange(index, "description", e.target.value)}
- placeholder="설명"
- className="h-7 text-xs"
- />
-
-
- ))}
-
-
- {/* 로딩 표시 */}
- {columnsLoading && (
-
-
- 더 많은 컬럼 로딩 중...
+ ))}
- )}
- {/* 페이지 정보 */}
-
- {columns.length} / {totalColumns} 컬럼 표시됨
-
+ {/* 로딩 표시 */}
+ {columnsLoading && (
+
+
+ 더 많은 컬럼 로딩 중...
+
+ )}
- {/* 전체 저장 버튼 */}
-
-
-
- 전체 설정 저장
-
+ {/* 페이지 정보 */}
+
+ {columns.length} / {totalColumns} 컬럼 표시됨
+
+
+ {/* 전체 저장 버튼 */}
+
+
+
+ 전체 설정 저장
+
+
-
- )}
- >
- )}
-
-
+ )}
+ >
+ )}
+
+
+
+
+
+ {/* DDL 모달 컴포넌트들 */}
+ {isSuperAdmin && (
+ <>
+
setCreateTableModalOpen(false)}
+ onSuccess={async (result) => {
+ toast.success("테이블이 성공적으로 생성되었습니다!");
+ // 테이블 목록 새로고침
+ await loadTables();
+ // 새로 생성된 테이블 자동 선택 및 컬럼 로드
+ if (result.data?.tableName) {
+ setSelectedTable(result.data.tableName);
+ setCurrentPage(1);
+ setColumns([]);
+ await loadColumnTypes(result.data.tableName, 1, pageSize);
+ }
+ }}
+ />
+
+ setAddColumnModalOpen(false)}
+ tableName={selectedTable || ""}
+ onSuccess={async (result) => {
+ toast.success("컬럼이 성공적으로 추가되었습니다!");
+ // 테이블 목록 새로고침 (컬럼 수 업데이트)
+ await loadTables();
+ // 선택된 테이블의 컬럼 목록 새로고침 - 페이지 리셋
+ if (selectedTable) {
+ setCurrentPage(1);
+ setColumns([]); // 기존 컬럼 목록 초기화
+ await loadColumnTypes(selectedTable, 1, pageSize);
+ }
+ }}
+ />
+
+ setDdlLogViewerOpen(false)} />
+ >
+ )}
+
+ {/* Scroll to Top 버튼 */}
+
-
- {/* DDL 모달 컴포넌트들 */}
- {isSuperAdmin && (
- <>
-
setCreateTableModalOpen(false)}
- onSuccess={async (result) => {
- toast.success("테이블이 성공적으로 생성되었습니다!");
- // 테이블 목록 새로고침
- await loadTables();
- // 새로 생성된 테이블 자동 선택 및 컬럼 로드
- if (result.data?.tableName) {
- setSelectedTable(result.data.tableName);
- setCurrentPage(1);
- setColumns([]);
- await loadColumnTypes(result.data.tableName, 1, pageSize);
- }
- }}
- />
-
- setAddColumnModalOpen(false)}
- tableName={selectedTable || ""}
- onSuccess={async (result) => {
- toast.success("컬럼이 성공적으로 추가되었습니다!");
- // 테이블 목록 새로고침 (컬럼 수 업데이트)
- await loadTables();
- // 선택된 테이블의 컬럼 목록 새로고침 - 페이지 리셋
- if (selectedTable) {
- setCurrentPage(1);
- setColumns([]); // 기존 컬럼 목록 초기화
- await loadColumnTypes(selectedTable, 1, pageSize);
- }
- }}
- />
-
- setDdlLogViewerOpen(false)} />
- >
- )}
);
}
diff --git a/frontend/app/(main)/admin/userMng/page.tsx b/frontend/app/(main)/admin/userMng/page.tsx
index d17db124..428e8986 100644
--- a/frontend/app/(main)/admin/userMng/page.tsx
+++ b/frontend/app/(main)/admin/userMng/page.tsx
@@ -1,24 +1,30 @@
"use client";
import { UserManagement } from "@/components/admin/UserManagement";
+import { ScrollToTop } from "@/components/common/ScrollToTop";
/**
* 사용자관리 페이지
* URL: /admin/userMng
+ *
+ * shadcn/ui 스타일 가이드 적용
*/
export default function UserMngPage() {
return (
-
-
- {/* 페이지 제목 */}
-
-
-
사용자 관리
-
시스템 사용자 계정 및 권한을 관리합니다
-
+
+
+ {/* 페이지 헤더 */}
+
+
사용자 관리
+
시스템 사용자 계정 및 권한을 관리합니다
+
+ {/* 메인 컨텐츠 */}
+
+ {/* Scroll to Top 버튼 (모바일/태블릿 전용) */}
+
);
}
diff --git a/frontend/components/admin/BatchCard.tsx b/frontend/components/admin/BatchCard.tsx
index ed8dd94e..374c81a2 100644
--- a/frontend/components/admin/BatchCard.tsx
+++ b/frontend/components/admin/BatchCard.tsx
@@ -12,8 +12,6 @@ import {
RefreshCw,
Clock,
Database,
- ArrowRight,
- Globe,
Calendar,
Activity,
Settings
@@ -39,90 +37,97 @@ export default function BatchCard({
onDelete,
getMappingSummary
}: BatchCardProps) {
- // 상태에 따른 색상 및 스타일 결정
- const getStatusColor = () => {
- if (executingBatch === batch.id) return "bg-blue-50 border-blue-200";
- if (batch.is_active === 'Y') return "bg-green-50 border-green-200";
- return "bg-gray-50 border-gray-200";
- };
-
- const getStatusBadge = () => {
- if (executingBatch === batch.id) {
- return
실행 중 ;
- }
- return (
-
- {batch.is_active === 'Y' ? '활성' : '비활성'}
-
- );
- };
+ // 상태에 따른 스타일 결정
+ const isExecuting = executingBatch === batch.id;
+ const isActive = batch.is_active === 'Y';
return (
-
-
- {/* 헤더 섹션 */}
-
-
-
-
-
{batch.batch_name}
+
+
+ {/* 헤더 */}
+
+
+
+
+
{batch.batch_name}
- {getStatusBadge()}
+
+ {batch.description || '설명 없음'}
+
-
-
- {batch.description || '\u00A0'}
-
+
+ {isExecuting ? '실행 중' : isActive ? '활성' : '비활성'}
+
- {/* 정보 섹션 */}
-
+ {/* 정보 */}
+
{/* 스케줄 정보 */}
-
-
-
{batch.cron_schedule}
+
+
+
+ 스케줄
+
+ {batch.cron_schedule}
{/* 생성일 정보 */}
-
-
-
+
+
+
+ 생성일
+
+
{new Date(batch.created_date).toLocaleDateString('ko-KR')}
-
- {/* 매핑 정보 섹션 */}
- {batch.batch_mappings && batch.batch_mappings.length > 0 && (
-
-
-
-
- 매핑 ({batch.batch_mappings.length})
+ {/* 매핑 정보 */}
+ {batch.batch_mappings && batch.batch_mappings.length > 0 && (
+
+
+
+ 매핑
+
+
+ {batch.batch_mappings.length}개
-
- {getMappingSummary(batch.batch_mappings)}
+ )}
+
+
+ {/* 실행 중 프로그레스 */}
+ {isExecuting && (
+
)}
- {/* 액션 버튼 섹션 */}
-
+ {/* 액션 버튼 */}
+
{/* 실행 버튼 */}
onExecute(batch.id)}
- disabled={executingBatch === batch.id}
- className="flex items-center justify-center space-x-1 bg-blue-50 hover:bg-blue-100 text-blue-700 border-blue-200 text-xs h-6"
+ disabled={isExecuting}
+ className="h-9 flex-1 gap-2 text-sm"
>
- {executingBatch === batch.id ? (
-
+ {isExecuting ? (
+
) : (
-
+
)}
- 실행
+ 실행
{/* 활성화/비활성화 버튼 */}
@@ -130,18 +135,14 @@ export default function BatchCard({
variant="outline"
size="sm"
onClick={() => onToggleStatus(batch.id, batch.is_active)}
- className={`flex items-center justify-center space-x-1 text-xs h-6 ${
- batch.is_active === 'Y'
- ? 'bg-orange-50 hover:bg-orange-100 text-orange-700 border-orange-200'
- : 'bg-green-50 hover:bg-green-100 text-green-700 border-green-200'
- }`}
+ className="h-9 flex-1 gap-2 text-sm"
>
- {batch.is_active === 'Y' ? (
-
+ {isActive ? (
+
) : (
-
+
)}
-
{batch.is_active === 'Y' ? '비활성' : '활성'}
+ {isActive ? '비활성' : '활성'}
{/* 수정 버튼 */}
@@ -149,36 +150,23 @@ export default function BatchCard({
variant="outline"
size="sm"
onClick={() => onEdit(batch.id)}
- className="flex items-center justify-center space-x-1 bg-gray-50 hover:bg-gray-100 text-gray-700 border-gray-200 text-xs h-6"
+ className="h-9 flex-1 gap-2 text-sm"
>
-
-
수정
+
+ 수정
{/* 삭제 버튼 */}
onDelete(batch.id, batch.batch_name)}
- className="flex items-center justify-center space-x-1 bg-red-50 hover:bg-red-100 text-red-700 border-red-200 text-xs h-6"
+ className="h-9 flex-1 gap-2 text-sm"
>
-
- 삭제
+
+ 삭제
-
- {/* 실행 중일 때 프로그레스 표시 */}
- {executingBatch === batch.id && (
-
- )}
);
diff --git a/frontend/components/admin/BatchJobModal.tsx b/frontend/components/admin/BatchJobModal.tsx
index e7f75f77..b7da440e 100644
--- a/frontend/components/admin/BatchJobModal.tsx
+++ b/frontend/components/admin/BatchJobModal.tsx
@@ -166,41 +166,25 @@ export default function BatchJobModal({
}));
};
- const getJobTypeIcon = (type: string) => {
- switch (type) {
- case 'collection': return '📥';
- case 'sync': return '🔄';
- case 'cleanup': return '🧹';
- case 'custom': return '⚙️';
- default: return '📋';
- }
- };
-
- const getStatusColor = (status: string) => {
- switch (status) {
- case 'Y': return 'bg-green-100 text-green-800';
- case 'N': return 'bg-destructive/20 text-red-800';
- default: return 'bg-gray-100 text-gray-800';
- }
- };
+ // 상태 제거 - 필요없음
return (
-
+
-
+
{job ? "배치 작업 수정" : "새 배치 작업"}
-