From a0d8605526ce87555a1f66a71413bb15018367ac Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sat, 7 Mar 2026 04:09:49 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260306183434-ewn8 round-1 --- .../app/(main)/admin/approvalBox/page.tsx | 530 +++++++++++++----- .../(main)/admin/approvalTemplate/page.tsx | 190 +++++-- frontend/app/(main)/admin/audit-log/page.tsx | 18 +- .../(main)/admin/batch-management/page.tsx | 362 +++++++----- frontend/app/(main)/admin/standards/page.tsx | 521 ++++++++++------- frontend/components/common/DataTable.tsx | 8 +- frontend/components/common/DualListBox.tsx | 40 +- .../components/common/EditableSpreadsheet.tsx | 8 +- frontend/components/v2/V2Date.tsx | 24 +- frontend/components/v2/V2Group.tsx | 27 +- frontend/components/v2/V2Input.tsx | 32 +- frontend/components/v2/V2Layout.tsx | 44 +- frontend/components/v2/V2Repeater.tsx | 10 +- frontend/components/v2/V2Select.tsx | 34 +- run-e2e-smoke.sh | 5 + 15 files changed, 1225 insertions(+), 628 deletions(-) create mode 100644 run-e2e-smoke.sh diff --git a/frontend/app/(main)/admin/approvalBox/page.tsx b/frontend/app/(main)/admin/approvalBox/page.tsx index 1d2e11d5..e7488808 100644 --- a/frontend/app/(main)/admin/approvalBox/page.tsx +++ b/frontend/app/(main)/admin/approvalBox/page.tsx @@ -118,47 +118,126 @@ function SentTab() { return (
{loading ? ( -
- -
+ <> + {/* 데스크톱 스켈레톤 */} +
+ + + + 제목 + 대상 테이블 + 진행 + 상태 + 요청일 + 보기 + + + + {Array.from({ length: 5 }).map((_, i) => ( + +
+
+
+
+
+
+ + ))} + +
+
+ {/* 모바일 스켈레톤 */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, j) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ ) : requests.length === 0 ? ( -
+

상신한 결재가 없습니다.

) : ( -
- - - - 제목 - 대상 테이블 - 진행 - 상태 - 요청일 - 보기 - - - - {requests.map((req) => ( - - {req.title} - {req.target_table} - - {req.current_step}/{req.total_steps} - - - {formatDate(req.created_at)} - - - + <> + {/* 데스크톱 테이블 */} +
+
+ + + 제목 + 대상 테이블 + 진행 + 상태 + 요청일 + 보기 - ))} - -
-
+ + + {requests.map((req) => ( + + {req.title} + {req.target_table} + + {req.current_step}/{req.total_steps} + + + {formatDate(req.created_at)} + + + + + ))} + + +
+ {/* 모바일 카드 */} +
+ {requests.map((req) => ( +
+
+

{req.title}

+ +
+
+
+ 대상 테이블 + {req.target_table || "-"} +
+
+ 진행 + {req.current_step}/{req.total_steps} +
+
+ 요청일 + {formatDate(req.created_at)} +
+
+
+ +
+
+ ))} +
+ )} {/* 상세 모달 */} @@ -306,52 +385,135 @@ function ReceivedTab() { return (
{loading ? ( -
- -
+ <> + {/* 데스크톱 스켈레톤 */} +
+ + + + 제목 + 요청자 + 대상 테이블 + 단계 + 요청일 + 처리 + + + + {Array.from({ length: 5 }).map((_, i) => ( + +
+
+
+
+
+
+ + ))} + +
+
+ {/* 모바일 스켈레톤 */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, j) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ ) : pendingLines.length === 0 ? ( -
+

결재 대기 건이 없습니다.

) : ( -
- - - - 제목 - 요청자 - 대상 테이블 - 단계 - 요청일 - 처리 - - - - {pendingLines.map((line) => ( - - {line.title || "-"} - - {line.requester_name || "-"} - {line.requester_dept && ( - ({line.requester_dept}) - )} - - {line.target_table || "-"} - - {line.step_order}차 - - {formatDate(line.request_created_at || line.created_at)} - - - + <> + {/* 데스크톱 테이블 */} +
+
+ + + 제목 + 요청자 + 대상 테이블 + 단계 + 요청일 + 처리 - ))} - -
-
+ + + {pendingLines.map((line) => ( + + {line.title || "-"} + + {line.requester_name || "-"} + {line.requester_dept && ( + ({line.requester_dept}) + )} + + {line.target_table || "-"} + + {line.step_order}차 + + {formatDate(line.request_created_at || line.created_at)} + + + + + ))} + + +
+ {/* 모바일 카드 */} +
+ {pendingLines.map((line) => ( +
+
+

{line.title || "-"}

+ {line.step_order}차 +
+
+
+ 요청자 + + {line.requester_name || "-"} + {line.requester_dept && ( + ({line.requester_dept}) + )} + +
+
+ 대상 테이블 + {line.target_table || "-"} +
+
+ 요청일 + {formatDate(line.request_created_at || line.created_at)} +
+
+
+ +
+
+ ))} +
+ )} {/* 결재 처리 모달 */} @@ -732,7 +894,7 @@ function ProxyTab() { return (
{/* 상단 액션 */} -
+

결재자가 부재 시 대결자가 대신 결재를 처리합니다.

@@ -744,72 +906,168 @@ function ProxyTab() { {/* 목록 */} {loading ? ( -
- -
+ <> + {/* 데스크톱 스켈레톤 */} +
+ + + + 원래 결재자 + 대결자 + 시작일 + 종료일 + 사유 + 활성 + 관리 + + + + {Array.from({ length: 4 }).map((_, i) => ( + +
+
+
+
+
+
+
+ + ))} + +
+
+ {/* 모바일 스켈레톤 */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, j) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ ) : proxies.length === 0 ? ( -
+

등록된 대결 설정이 없습니다.

) : ( -
- - - - 원래 결재자 - 대결자 - 시작일 - 종료일 - 사유 - 활성 - 관리 - - - - {proxies.map((proxy) => ( - - - {proxy.original_user_name || proxy.original_user_id} - {proxy.original_dept_name && ( - ({proxy.original_dept_name}) - )} - - - {proxy.proxy_user_name || proxy.proxy_user_id} - {proxy.proxy_dept_name && ( - ({proxy.proxy_dept_name}) - )} - - - {formatDateOnly(proxy.start_date)} - - - {formatDateOnly(proxy.end_date)} - - - {proxy.reason || "-"} - - - - {proxy.is_active === "Y" ? "활성" : "비활성"} - - - -
- - -
-
+ <> + {/* 데스크톱 테이블 */} +
+
+ + + 원래 결재자 + 대결자 + 시작일 + 종료일 + 사유 + 활성 + 관리 - ))} - -
-
+ + + {proxies.map((proxy) => ( + + + {proxy.original_user_name || proxy.original_user_id} + {proxy.original_dept_name && ( + ({proxy.original_dept_name}) + )} + + + {proxy.proxy_user_name || proxy.proxy_user_id} + {proxy.proxy_dept_name && ( + ({proxy.proxy_dept_name}) + )} + + + {formatDateOnly(proxy.start_date)} + + + {formatDateOnly(proxy.end_date)} + + + {proxy.reason || "-"} + + + + {proxy.is_active === "Y" ? "활성" : "비활성"} + + + +
+ + +
+
+
+ ))} +
+ +
+ {/* 모바일 카드 */} +
+ {proxies.map((proxy) => ( +
+
+
+

+ {proxy.original_user_name || proxy.original_user_id} +

+

+ 대결: {proxy.proxy_user_name || proxy.proxy_user_id} +

+
+ + {proxy.is_active === "Y" ? "활성" : "비활성"} + +
+
+
+ 시작일 + {formatDateOnly(proxy.start_date)} +
+
+ 종료일 + {formatDateOnly(proxy.end_date)} +
+ {proxy.reason && ( +
+ 사유 + {proxy.reason} +
+ )} +
+
+ + +
+
+ ))} +
+ )} {/* 등록/수정 모달 */} diff --git a/frontend/app/(main)/admin/approvalTemplate/page.tsx b/frontend/app/(main)/admin/approvalTemplate/page.tsx index 7b4c9814..bf13602b 100644 --- a/frontend/app/(main)/admin/approvalTemplate/page.tsx +++ b/frontend/app/(main)/admin/approvalTemplate/page.tsx @@ -755,65 +755,149 @@ export default function ApprovalTemplatePage() {
- {/* 템플릿 목록 테이블 */} + {/* 템플릿 목록 */} {loading ? ( -
- -
+ <> + {/* 데스크톱 스켈레톤 */} +
+ + + + 템플릿명 + 설명 + 단계 구성 + 연결된 유형 + 생성일 + 관리 + + + + {Array.from({ length: 5 }).map((_, i) => ( + +
+
+
+
+
+
+ + ))} + +
+
+ {/* 모바일 스켈레톤 */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, j) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ ) : filtered.length === 0 ? ( -
+

등록된 결재 템플릿이 없습니다.

) : ( -
- - - - 템플릿명 - 설명 - 단계 구성 - 연결된 유형 - 생성일 - 관리 - - - - {filtered.map((tpl) => ( - - {tpl.template_name} - - {tpl.description || "-"} - - {renderStepSummary(tpl)} - {tpl.definition_name || "-"} - - {formatDate(tpl.created_at)} - - -
- - -
-
+ <> + {/* 데스크톱 테이블 */} +
+
+ + + 템플릿명 + 설명 + 단계 구성 + 연결된 유형 + 생성일 + 관리 - ))} - -
-
+ + + {filtered.map((tpl) => ( + + {tpl.template_name} + + {tpl.description || "-"} + + {renderStepSummary(tpl)} + {tpl.definition_name || "-"} + + {formatDate(tpl.created_at)} + + +
+ + +
+
+
+ ))} +
+ +
+ {/* 모바일 카드 */} +
+ {filtered.map((tpl) => ( +
+
+

{tpl.template_name}

+ {tpl.definition_name && ( + {tpl.definition_name} + )} +
+ {tpl.description && ( +

{tpl.description}

+ )} +
+
+ 단계 구성 +
{renderStepSummary(tpl)}
+
+
+ 생성일 + {formatDate(tpl.created_at)} +
+
+
+ + +
+
+ ))} +
+ )}
diff --git a/frontend/app/(main)/admin/audit-log/page.tsx b/frontend/app/(main)/admin/audit-log/page.tsx index a6f4493d..aa72386f 100644 --- a/frontend/app/(main)/admin/audit-log/page.tsx +++ b/frontend/app/(main)/admin/audit-log/page.tsx @@ -460,9 +460,9 @@ export default function AuditLogPage() {
-
+
@@ -475,7 +475,7 @@ export default function AuditLogPage() {
-
+
{isSuperAdmin && ( -
+
@@ -604,7 +604,7 @@ export default function AuditLogPage() {
)} -
+
@@ -685,7 +685,7 @@ export default function AuditLogPage() {
-
+
-
+
- diff --git a/frontend/app/(main)/admin/batch-management/page.tsx b/frontend/app/(main)/admin/batch-management/page.tsx index b9ed4230..666dfa69 100644 --- a/frontend/app/(main)/admin/batch-management/page.tsx +++ b/frontend/app/(main)/admin/batch-management/page.tsx @@ -43,6 +43,7 @@ import { import { toast } from "sonner"; import { BatchAPI, BatchJob } from "@/lib/api/batch"; import BatchJobModal from "@/components/admin/BatchJobModal"; +import { showErrorToast } from "@/lib/utils/toastUtils"; export default function BatchManagementPage() { const router = useRouter(); @@ -170,33 +171,16 @@ export default function BatchManagementPage() { const getStatusBadge = (isActive: string) => { return isActive === "Y" ? ( - 활성 + 활성 ) : ( - 비활성 + 비활성 ); }; const getTypeBadge = (type: string) => { const option = jobTypes.find(opt => opt.value === type); - const colors = { - collection: "bg-blue-100 text-blue-800", - sync: "bg-purple-100 text-purple-800", - cleanup: "bg-orange-100 text-orange-800", - custom: "bg-gray-100 text-gray-800", - }; - - const icons = { - collection: "📥", - sync: "🔄", - cleanup: "🧹", - custom: "⚙️", - }; - return ( - - {icons[type as keyof typeof icons] || "📋"} - {option?.label || type} - + {option?.label || type} ); }; @@ -228,11 +212,10 @@ export default function BatchManagementPage() {
{/* 통계 카드 */} -
+
총 작업 -
📋
{jobs.length}
@@ -245,7 +228,6 @@ export default function BatchManagementPage() { 총 실행 -
▶️
@@ -258,10 +240,9 @@ export default function BatchManagementPage() { 성공 -
-
+
{jobs.reduce((sum, job) => sum + job.success_count, 0)}

총 성공 횟수

@@ -271,10 +252,9 @@ export default function BatchManagementPage() { 실패 -
-
+
{jobs.reduce((sum, job) => sum + job.failure_count, 0)}

총 실패 횟수

@@ -283,132 +263,168 @@ export default function BatchManagementPage() {
{/* 필터 및 검색 */} - - - 필터 및 검색 - - -
-
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
-
- - - - - - +
+
+
+ + setSearchTerm(e.target.value)} + className="h-10 pl-10 text-sm" + />
- - - {/* 배치 작업 목록 */} - - - 배치 작업 목록 ({filteredJobs.length}개) - - - {isLoading ? ( -
- -

배치 작업을 불러오는 중...

-
- ) : filteredJobs.length === 0 ? ( -
- {jobs.length === 0 ? "배치 작업이 없습니다." : "검색 결과가 없습니다."} -
- ) : ( + + + +
+ + +
+ + {/* 배치 작업 목록 제목 */} +
+ 총 {filteredJobs.length}개 +
+ + {isLoading ? ( + <> + {/* 데스크톱 스켈레톤 */} +
- - 작업명 - 타입 - 스케줄 - 상태 - 실행 통계 - 성공률 - 마지막 실행 - 작업 + + 작업명 + 타입 + 스케줄 + 상태 + 실행 통계 + 성공률 + 마지막 실행 + 작업 + + + + {Array.from({ length: 5 }).map((_, i) => ( + +
+
+
+
+
+
+
+
+
+ ))} +
+
+
+ {/* 모바일 스켈레톤 */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, j) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ + ) : filteredJobs.length === 0 ? ( +
+ {jobs.length === 0 ? "배치 작업이 없습니다." : "검색 결과가 없습니다."} +
+ ) : ( + <> + {/* 데스크톱 테이블 */} +
+ + + + 작업명 + 타입 + 스케줄 + 상태 + 실행 통계 + 성공률 + 마지막 실행 + 작업 {filteredJobs.map((job) => ( - - + +
{job.job_name}
{job.description && ( -
- {job.description} -
+
{job.description}
)}
- - {getTypeBadge(job.job_type)} - - - {job.schedule_cron || "-"} - - - {getStatusBadge(job.is_active)} - - -
+ {getTypeBadge(job.job_type)} + {job.schedule_cron || "-"} + {getStatusBadge(job.is_active)} + +
총 {job.execution_count}회
-
+
성공 {job.success_count} / 실패 {job.failure_count}
- -
-
= 90 ? 'text-green-600' : - getSuccessRate(job) >= 70 ? 'text-yellow-600' : 'text-red-600' - }`}> - {getSuccessRate(job)}% -
-
+ + = 90 ? 'text-success' : + getSuccessRate(job) >= 70 ? 'text-warning' : 'text-destructive' + }`}> + {getSuccessRate(job)}% + - + {job.last_executed_at ? new Date(job.last_executed_at).toLocaleString() : "-"} - +
- )} - - +
+ + {/* 모바일 카드 */} +
+ {filteredJobs.map((job) => ( +
+
+
+

{job.job_name}

+ {job.description && ( +

{job.description}

+ )} +
+
{getStatusBadge(job.is_active)}
+
+
+
+ 타입 + {getTypeBadge(job.job_type)} +
+
+ 스케줄 + {job.schedule_cron || "-"} +
+
+ 실행 횟수 + {job.execution_count}회 +
+
+ 성공률 + = 90 ? 'text-success' : + getSuccessRate(job) >= 70 ? 'text-warning' : 'text-destructive' + }`}> + {getSuccessRate(job)}% + +
+
+ 마지막 실행 + + {job.last_executed_at + ? new Date(job.last_executed_at).toLocaleDateString() + : "-"} + +
+
+
+ + + +
+
+ ))} +
+ + )} {/* 배치 타입 선택 모달 */} {isBatchTypeModalOpen && ( diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index 71a63371..67a43bab 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -4,7 +4,6 @@ import React, { useState, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { @@ -20,7 +19,7 @@ import { } from "@/components/ui/alert-dialog"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/utils/toastUtils"; -import { Plus, Search, Edit, Trash2, Eye, Filter, RotateCcw, Settings, SortAsc, SortDesc } from "lucide-react"; +import { Plus, Search, Edit, Trash2, Eye, RotateCcw, SortAsc, SortDesc } from "lucide-react"; import { useWebTypes } from "@/hooks/admin/useWebTypes"; import Link from "next/link"; @@ -104,230 +103,241 @@ export default function WebTypesManagePage() { setSortDirection("asc"); }; - // 로딩 상태 - if (isLoading) { - return ( -
-
웹타입 목록을 불러오는 중...
+ return ( +
+ {/* 페이지 헤더 */} +
+
+

웹타입 관리

+

화면관리에서 사용할 웹타입들을 관리합니다

+
+ + +
- ); - } - // 에러 상태 - if (error) { - return ( -
-
-
웹타입 목록을 불러오는데 실패했습니다.
-
+ )} + + {/* 검색 툴바 */} +
+
+
+ + setSearchTerm(e.target.value)} + className="h-10 pl-10 text-sm" + /> +
+ + + + +
+ +
- ); - } - return ( -
-
- {/* 페이지 제목 */} -
-
-

웹타입 관리

-

화면관리에서 사용할 웹타입들을 관리합니다

-
- - - + {/* 결과 수 */} +
+ 총 {filteredAndSortedWebTypes.length}개의 웹타입 +
+ + {/* 삭제 에러 */} + {deleteError && ( +
+

+ 삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"} +

+ )} - {/* 필터 및 검색 */} - - - - - 필터 및 검색 - - - -
- {/* 검색 */} -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
- - {/* 카테고리 필터 */} - - - {/* 활성화 상태 필터 */} - - - {/* 초기화 버튼 */} - -
-
-
- - {/* 결과 통계 */} -
-

총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.

-
- - {/* 웹타입 목록 테이블 */} -
- - - - handleSort("sort_order")}> -
- 순서 - {sortField === "sort_order" && - (sortDirection === "asc" ? : )} -
-
- handleSort("web_type")}> -
- 웹타입 코드 - {sortField === "web_type" && - (sortDirection === "asc" ? : )} -
-
- handleSort("type_name")}> -
- 웹타입명 - {sortField === "type_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("category")}> -
- 카테고리 - {sortField === "category" && - (sortDirection === "asc" ? : )} -
-
- 설명 - handleSort("component_name")}> -
- 연결된 컴포넌트 - {sortField === "component_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("config_panel")}> -
- 설정 패널 - {sortField === "config_panel" && - (sortDirection === "asc" ? : )} -
-
- handleSort("is_active")}> -
- 상태 - {sortField === "is_active" && - (sortDirection === "asc" ? : )} -
-
- handleSort("updated_date")}> -
- 최종 수정일 - {sortField === "updated_date" && - (sortDirection === "asc" ? : )} -
-
- 작업 -
-
- - {filteredAndSortedWebTypes.length === 0 ? ( - - - 조건에 맞는 웹타입이 없습니다. - + {isLoading ? ( + <> + {/* 데스크톱 스켈레톤 */} +
+
+ + + 순서 + 웹타입 코드 + 웹타입명 + 카테고리 + 설명 + 상태 + 최종 수정일 + 작업 - ) : ( - filteredAndSortedWebTypes.map((webType) => ( - - {webType.sort_order || 0} - {webType.web_type} - - {webType.type_name} + + + {Array.from({ length: 6 }).map((_, i) => ( + +
+
+
+
+
+
+
+
+
+ ))} +
+
+
+ {/* 모바일 스켈레톤 */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, j) => ( +
+
+
+
+ ))} +
+
+ ))} +
+ + ) : filteredAndSortedWebTypes.length === 0 ? ( +
+ 조건에 맞는 웹타입이 없습니다. +
+ ) : ( + <> + {/* 데스크톱 테이블 */} +
+ + + + handleSort("sort_order")}> +
+ 순서 + {sortField === "sort_order" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("web_type")}> +
+ 웹타입 코드 + {sortField === "web_type" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("type_name")}> +
+ 웹타입명 + {sortField === "type_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("category")}> +
+ 카테고리 + {sortField === "category" && + (sortDirection === "asc" ? : )} +
+
+ 설명 + handleSort("is_active")}> +
+ 상태 + {sortField === "is_active" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("updated_date")}> +
+ 최종 수정일 + {sortField === "updated_date" && + (sortDirection === "asc" ? : )} +
+
+ 작업 +
+
+ + {filteredAndSortedWebTypes.map((webType) => ( + + {webType.sort_order || 0} + {webType.web_type} + +
{webType.type_name}
{webType.type_name_eng && ( -
{webType.type_name_eng}
+
{webType.type_name_eng}
)}
- + {webType.category} - {webType.description || "-"} - - - {webType.component_name || "TextWidget"} - - - - - {webType.config_panel === "none" || !webType.config_panel ? "기본 설정" : webType.config_panel} - - - + {webType.description || "-"} + {webType.is_active === "Y" ? "활성화" : "비활성화"} - + {webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"} - -
- - - - - - - - - - + +
+ + + + + + + + + + 웹타입 삭제 @@ -351,20 +361,97 @@ export default function WebTypesManagePage() {
- )) - )} - -
-
+ ))} + + +
- {deleteError && ( -
-

- 삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"} -

-
+ {/* 모바일 카드 */} +
+ {filteredAndSortedWebTypes.map((webType) => ( +
+
+
+

{webType.type_name}

+ {webType.type_name_eng && ( +

{webType.type_name_eng}

+ )} +

{webType.web_type}

+
+ + {webType.is_active === "Y" ? "활성화" : "비활성화"} + +
+
+
+ 카테고리 + {webType.category} +
+
+ 순서 + {webType.sort_order || 0} +
+ {webType.description && ( +
+ 설명 + {webType.description} +
+ )} +
+ 수정일 + + {webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"} + +
+
+
+ + + + + + + + + + + + + 웹타입 삭제 + + '{webType.type_name}' 웹타입을 삭제하시겠습니까? +
이 작업은 되돌릴 수 없습니다. +
+
+ + 취소 + handleDelete(webType.web_type, webType.type_name)} + disabled={isDeleting} + className="bg-destructive hover:bg-destructive/90" + > + {isDeleting ? "삭제 중..." : "삭제"} + + +
+
+
+
+ ))} +
+ )} -
); } diff --git a/frontend/components/common/DataTable.tsx b/frontend/components/common/DataTable.tsx index cd7d1a66..54da4617 100644 --- a/frontend/components/common/DataTable.tsx +++ b/frontend/components/common/DataTable.tsx @@ -259,12 +259,12 @@ export const createStatusColumn = (accessorKey: string, header: string) => ({ className={cn( "inline-flex items-center rounded-full px-2 py-1 text-xs font-medium", status === "active" || status === "활성" - ? "bg-green-50 text-green-700" + ? "bg-success/10 text-success" : status === "inactive" || status === "비활성" - ? "bg-gray-50 text-gray-700" + ? "bg-muted text-muted-foreground" : status === "pending" || status === "대기" - ? "bg-yellow-50 text-yellow-700" - : "bg-destructive/10 text-red-700", + ? "bg-warning/10 text-warning" + : "bg-destructive/10 text-destructive", )} > {status || "-"} diff --git a/frontend/components/common/DualListBox.tsx b/frontend/components/common/DualListBox.tsx index 36582e8d..c5d25a2e 100644 --- a/frontend/components/common/DualListBox.tsx +++ b/frontend/components/common/DualListBox.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; -import { ChevronRight, ChevronLeft, ChevronsRight, ChevronsLeft, Search } from "lucide-react"; +import { ChevronRight, ChevronLeft, ChevronDown, ChevronUp, ChevronsRight, ChevronsLeft, ChevronsDown, ChevronsUp, Search } from "lucide-react"; import { cn } from "@/lib/utils"; /** @@ -197,9 +197,9 @@ export function DualListBox({ const itemRenderer = renderItem || defaultRenderItem; return ( -
+
{/* 좌측 리스트 (사용 가능한 항목) */} -
+
{/* 검색 */} @@ -210,7 +210,7 @@ export function DualListBox({ placeholder="검색..." value={leftSearch} onChange={(e) => setLeftSearch(e.target.value)} - className="h-9 pl-9 text-sm" + className="h-9 w-full pl-9 text-sm" disabled={disabled} />
@@ -263,55 +263,57 @@ export function DualListBox({
- {/* 중앙 버튼 (이동) */} -
+ {/* 중앙 버튼 (이동) - 모바일: 가로 배치, 데스크톱: 세로 배치 */} +
+ {/* 모두 이동: 모바일은 아래/위, 데스크톱은 오른쪽/왼쪽 */}
{/* 우측 리스트 (선택된 항목) */} -
+
{/* 검색 */} @@ -322,7 +324,7 @@ export function DualListBox({ placeholder="검색..." value={rightSearch} onChange={(e) => setRightSearch(e.target.value)} - className="h-9 pl-9 text-sm" + className="h-9 w-full pl-9 text-sm" disabled={disabled} />
diff --git a/frontend/components/common/EditableSpreadsheet.tsx b/frontend/components/common/EditableSpreadsheet.tsx index 67e7f80b..1c5078d5 100644 --- a/frontend/components/common/EditableSpreadsheet.tsx +++ b/frontend/components/common/EditableSpreadsheet.tsx @@ -992,10 +992,10 @@ export const EditableSpreadsheet: React.FC = ({
- +
{/* 열 인덱스 헤더 (A, B, C, ...) */} @@ -1066,7 +1066,7 @@ export const EditableSpreadsheet: React.FC = ({ onChange={(e) => setEditValue(e.target.value)} onKeyDown={handleKeyDown} onBlur={finishEditing} - className="w-full bg-white px-2 py-1 text-xs font-medium text-primary outline-none" + className="min-w-[80px] w-full bg-white px-2 py-1 text-xs font-medium text-primary outline-none" /> ) : (
{colName || 빈 헤더}
@@ -1124,7 +1124,7 @@ export const EditableSpreadsheet: React.FC = ({ onChange={(e) => setEditValue(e.target.value)} onKeyDown={handleKeyDown} onBlur={finishEditing} - className="w-full bg-white px-2 py-1 text-xs outline-none" + className="min-w-[80px] w-full bg-white px-2 py-1 text-xs outline-none" /> ) : (
((props, ref) => {
- {labelElement} - {dateContent} + +
+ {renderDatePicker()} +
); } diff --git a/frontend/components/v2/V2Group.tsx b/frontend/components/v2/V2Group.tsx index 8e156ff1..885d2be7 100644 --- a/frontend/components/v2/V2Group.tsx +++ b/frontend/components/v2/V2Group.tsx @@ -244,10 +244,10 @@ const ModalGroup = forwardRef(({ title, description, open = false, onOpenChange, modalSize = "md", children, className }, ref) => { const sizeClasses = { - sm: "max-w-sm", - md: "max-w-md", - lg: "max-w-lg", - xl: "max-w-xl", + sm: "max-w-[95vw] sm:max-w-sm", + md: "max-w-[95vw] sm:max-w-md", + lg: "max-w-[95vw] sm:max-w-lg", + xl: "max-w-[95vw] sm:max-w-xl", }; return ( @@ -297,10 +297,10 @@ const FormModalGroup = forwardRef { const sizeClasses = { - sm: "max-w-sm", - md: "max-w-md", - lg: "max-w-lg", - xl: "max-w-xl", + sm: "max-w-[95vw] sm:max-w-sm", + md: "max-w-[95vw] sm:max-w-md", + lg: "max-w-[95vw] sm:max-w-lg", + xl: "max-w-[95vw] sm:max-w-xl", }; const handleCancel = useCallback(() => { @@ -325,10 +325,17 @@ const FormModalGroup = forwardRef{children}
- - diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index 219fa275..c2313f5d 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -1023,17 +1023,39 @@ export const V2Input = forwardRef((props, ref) =>
- {labelElement} - {inputContent} + +
+ {renderInput()} +
); } diff --git a/frontend/components/v2/V2Layout.tsx b/frontend/components/v2/V2Layout.tsx index f58b9e93..01300bb9 100644 --- a/frontend/components/v2/V2Layout.tsx +++ b/frontend/components/v2/V2Layout.tsx @@ -31,20 +31,20 @@ const GridLayout = forwardRef(({ columns = 12, gap = "16px", children, className, use12Column = true }, ref) => { - // 12컬럼 그리드 클래스 매핑 + // 12컬럼 그리드 클래스 매핑 (모바일 단일 컬럼 → sm: 실제 컬럼 수) const gridColsClass: Record = { 1: "grid-cols-1", - 2: "grid-cols-2", - 3: "grid-cols-3", - 4: "grid-cols-4", - 5: "grid-cols-5", - 6: "grid-cols-6", - 7: "grid-cols-7", - 8: "grid-cols-8", - 9: "grid-cols-9", - 10: "grid-cols-10", - 11: "grid-cols-11", - 12: "grid-cols-12", + 2: "grid-cols-1 sm:grid-cols-2", + 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", + 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4", + 5: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-5", + 6: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-6", + 7: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-7", + 8: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-8", + 9: "grid-cols-1 sm:grid-cols-3 lg:grid-cols-9", + 10: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-10", + 11: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-11", + 12: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-12", }; // 12컬럼 시스템 사용 시 @@ -54,7 +54,7 @@ const GridLayout = forwardRef - {/* 첫 번째 패널 */} + {/* 첫 번째 패널 - 모바일에서 전체 너비 */}
- {/* 리사이저 */} + {/* 리사이저 - 모바일에서 숨김 */}
- {/* 두 번째 패널 */} + {/* 두 번째 패널 - 모바일에서 전체 너비 */}
= ({ return (
{/* 헤더 영역 */} -
+
{data.length > 0 && `${data.length}개 항목`} @@ -1636,19 +1636,19 @@ export const V2Repeater: React.FC = ({
{selectedRows.size > 0 && ( - )} -
- {/* Repeater 테이블 - 남은 공간에서 스크롤 */} -
+ {/* Repeater 테이블 - 남은 공간에서 스크롤, 모바일 가로 스크롤 */} +
(
- {labelElement} - {selectContent} + +
+ {renderSelect()} +
); } diff --git a/run-e2e-smoke.sh b/run-e2e-smoke.sh new file mode 100644 index 00000000..3cfde69b --- /dev/null +++ b/run-e2e-smoke.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd /Users/gbpark/ERP-node +node run-e2e-test.mjs 2>&1 | tee /tmp/e2e-smoke-result.txt +echo "EXIT_CODE: $?" >> /tmp/e2e-smoke-result.txt +cat /tmp/e2e-smoke-result.txt