From e56b7d53a404d443e70eca11d4642d43f4f8b1b2 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Sun, 11 Jan 2026 17:19:11 +0900 Subject: [PATCH] =?UTF-8?q?fix(WEB):=20=ED=86=A0=ED=81=B0=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EB=AC=B4=ED=95=9C=20=EB=A1=9C=EB=94=A9?= =?UTF-8?q?=20=EB=8C=80=EC=8B=A0=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 52개 이상의 컴포넌트에 isNextRedirectError 처리 추가 - Server Action의 redirect() 에러가 catch 블록에서 삼켜지는 문제 해결 - access_token + refresh_token 모두 만료 시 정상적으로 로그인 페이지로 리다이렉트 수정된 영역: - accounting: 10개 컴포넌트 - production: 12개 컴포넌트 - hr: 5개 컴포넌트 - settings: 8개 컴포넌트 - approval: 5개 컴포넌트 - items: 20개+ 컴포넌트 - board: 5개 컴포넌트 - quality: 4개 컴포넌트 - material, outbound, quotes 등 기타 컴포넌트 Co-Authored-By: Claude --- .../[PLAN] ceo-dashboard-refactoring.md | 331 ++++ .../[GUIDE] mobile-responsive-patterns.md | 538 +++++++ .../board/[boardCode]/[postId]/page.tsx | 8 +- .../board/board-management/[id]/edit/page.tsx | 7 +- .../board/board-management/[id]/page.tsx | 7 +- .../boards/[boardCode]/[postId]/edit/page.tsx | 7 +- .../(protected)/items/[id]/edit/page.tsx | 9 +- .../[locale]/(protected)/items/[id]/page.tsx | 9 +- .../(protected)/production/dashboard/page.tsx | 3 +- .../screen-production/[id]/edit/page.tsx | 7 +- .../screen-production/[id]/page.tsx | 7 +- .../production/worker-screen/page.tsx | 3 +- .../qms/components/Day1ChecklistPanel.tsx | 14 +- .../qms/components/Day1DocumentSection.tsx | 24 +- .../qms/components/Day1DocumentViewer.tsx | 40 +- .../quality/qms/components/DayTabs.tsx | 61 +- .../quality/qms/components/DocumentList.tsx | 14 +- .../quality/qms/components/Filters.tsx | 8 +- .../quality/qms/components/Header.tsx | 6 +- .../quality/qms/components/ReportList.tsx | 22 +- .../quality/qms/components/RouteList.tsx | 12 +- .../[locale]/(protected)/quality/qms/page.tsx | 18 +- .../BadDebtCollection/BadDebtDetail.tsx | 5 + .../BankTransactionInquiry/index.tsx | 2 + .../accounting/BillManagement/BillDetail.tsx | 5 +- .../CardTransactionInquiry/index.tsx | 2 + .../accounting/DailyReport/index.tsx | 16 +- .../accounting/ReceivablesStatus/index.tsx | 2 + .../SalesManagement/SalesDetail.tsx | 11 +- .../VendorLedger/VendorLedgerDetail.tsx | 13 +- .../accounting/VendorLedger/index.tsx | 2 + .../VendorManagement/VendorDetail.tsx | 10 + src/components/approval/ApprovalBox/index.tsx | 5 + .../DocumentCreate/ExpenseEstimateForm.tsx | 7 +- .../approval/DocumentCreate/index.tsx | 15 +- src/components/approval/DraftBox/index.tsx | 7 + .../approval/ReferenceBox/index.tsx | 5 + src/components/auth/LoginPage.tsx | 2 + src/components/auth/SignupPage.tsx | 2 + src/components/board/BoardDetail/index.tsx | 2 + src/components/board/BoardForm/index.tsx | 3 + src/components/board/BoardList/index.tsx | 11 +- .../board/BoardManagement/index.tsx | 7 +- .../board/RichTextEditor/MenuBar.tsx | 2 + .../business/CEODashboard/CEODashboard.tsx | 1364 +---------------- .../business/CEODashboard/components.tsx | 8 +- .../business/CEODashboard/mockData.ts | 350 +++++ .../modalConfigs/cardManagementConfigs.ts | 269 ++++ .../modalConfigs/entertainmentConfigs.ts | 231 +++ .../CEODashboard/modalConfigs/index.ts | 5 + .../modalConfigs/monthlyExpenseConfigs.ts | 317 ++++ .../CEODashboard/modalConfigs/vatConfigs.ts | 91 ++ .../modalConfigs/welfareConfigs.ts | 129 ++ .../CEODashboard/modals/DetailModal.tsx | 90 +- .../sections/CardManagementSection.tsx | 2 +- .../sections/DailyReportSection.tsx | 2 +- .../sections/EntertainmentSection.tsx | 2 +- .../sections/MonthlyExpenseSection.tsx | 2 +- .../sections/ReceivableSection.tsx | 2 +- .../sections/TodayIssueSection.tsx | 8 +- .../CEODashboard/sections/WelfareSection.tsx | 2 +- .../category-management/index.tsx | 12 +- .../contract/ContractDetailForm.tsx | 4 + src/components/common/DataTable/DataTable.tsx | 8 +- .../hr/AttendanceManagement/index.tsx | 11 +- .../hr/DepartmentManagement/index.tsx | 4 + .../hr/EmployeeManagement/index.tsx | 4 + src/components/hr/SalaryManagement/index.tsx | 6 + .../hr/VacationManagement/index.tsx | 8 + .../items/DynamicItemForm/index.tsx | 5 + .../sections/DynamicBOMSection.tsx | 2 + src/components/items/ItemDetailClient.tsx | 2 + src/components/items/ItemListClient.tsx | 2 + .../items/ItemMasterDataManagement.tsx | 33 +- .../components/ItemMasterDialogs.tsx | 124 +- .../dialogs/ColumnManageDialog.tsx | 24 +- .../dialogs/FieldDialog.tsx | 86 +- .../dialogs/FieldDrawer.tsx | 107 +- .../dialogs/ImportFieldDialog.tsx | 8 +- .../dialogs/ImportSectionDialog.tsx | 8 +- .../dialogs/LoadTemplateDialog.tsx | 5 +- .../dialogs/MasterFieldDialog.tsx | 30 +- .../dialogs/OptionDialog.tsx | 18 +- .../dialogs/SectionDialog.tsx | 36 +- .../dialogs/SectionTemplateDialog.tsx | 16 +- .../dialogs/TemplateFieldDialog.tsx | 68 +- .../hooks/useAttributeManagement.ts | 30 +- .../hooks/useDeleteManagement.ts | 3 + .../hooks/useFieldManagement.ts | 30 +- .../hooks/useImportManagement.ts | 4 + .../hooks/useMasterFieldManagement.ts | 25 +- .../hooks/useReorderManagement.ts | 3 + .../hooks/useSectionManagement.ts | 15 +- .../hooks/useTemplateManagement.ts | 33 +- .../tabs/HierarchyTab/index.tsx | 2 + .../items/ItemMasterDataManagement/types.ts | 8 +- .../ReceivingManagement/InspectionCreate.tsx | 9 +- .../ReceivingManagement/ReceivingDetail.tsx | 10 +- .../ReceivingManagement/ReceivingList.tsx | 10 +- .../StockStatus/StockStatusDetail.tsx | 9 +- .../material/StockStatus/StockStatusList.tsx | 10 +- .../ShipmentManagement/ShipmentCreate.tsx | 8 +- .../ShipmentManagement/ShipmentDetail.tsx | 8 +- .../ShipmentManagement/ShipmentEdit.tsx | 8 +- .../ShipmentManagement/ShipmentList.tsx | 10 +- src/components/pricing/PricingFormClient.tsx | 3 + .../production/ProductionDashboard/index.tsx | 11 +- .../WorkOrders/AssigneeSelectModal.tsx | 8 +- .../WorkOrders/SalesOrderSelectModal.tsx | 9 +- .../production/WorkOrders/WorkOrderCreate.tsx | 2 + .../production/WorkOrders/WorkOrderDetail.tsx | 9 +- .../production/WorkOrders/WorkOrderList.tsx | 2 + .../production/WorkResults/WorkResultList.tsx | 10 +- .../WorkerScreen/MaterialInputModal.tsx | 8 +- .../WorkerScreen/ProcessDetailSection.tsx | 8 +- .../production/WorkerScreen/index.tsx | 12 +- .../InspectionManagement/InspectionCreate.tsx | 2 + .../InspectionManagement/InspectionDetail.tsx | 8 +- .../InspectionManagement/InspectionList.tsx | 11 +- src/components/quotes/QuoteRegistration.tsx | 6 + .../reports/ComprehensiveAnalysis/index.tsx | 9 +- .../settings/LeavePolicyManagement/index.tsx | 6 +- .../PaymentHistoryClient.tsx | 2 + .../PermissionDetailClient.tsx | 16 +- .../settings/PermissionManagement/index.tsx | 8 +- .../settings/PopupManagement/PopupForm.tsx | 1 + .../settings/PopupManagement/PopupList.tsx | 41 +- .../settings/RankManagement/index.tsx | 12 +- .../settings/TitleManagement/index.tsx | 12 +- src/layouts/AuthenticatedLayout.tsx | 50 +- tsconfig.tsbuildinfo | 2 +- 131 files changed, 3320 insertions(+), 1979 deletions(-) create mode 100644 claudedocs/[PLAN] ceo-dashboard-refactoring.md create mode 100644 claudedocs/guides/[GUIDE] mobile-responsive-patterns.md create mode 100644 src/components/business/CEODashboard/mockData.ts create mode 100644 src/components/business/CEODashboard/modalConfigs/cardManagementConfigs.ts create mode 100644 src/components/business/CEODashboard/modalConfigs/entertainmentConfigs.ts create mode 100644 src/components/business/CEODashboard/modalConfigs/index.ts create mode 100644 src/components/business/CEODashboard/modalConfigs/monthlyExpenseConfigs.ts create mode 100644 src/components/business/CEODashboard/modalConfigs/vatConfigs.ts create mode 100644 src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts diff --git a/claudedocs/[PLAN] ceo-dashboard-refactoring.md b/claudedocs/[PLAN] ceo-dashboard-refactoring.md new file mode 100644 index 00000000..77f688c4 --- /dev/null +++ b/claudedocs/[PLAN] ceo-dashboard-refactoring.md @@ -0,0 +1,331 @@ +# CEO 대시보드 리팩토링 계획 + +> 작성일: 2026-01-10 +> 대상 파일: `src/components/business/CEODashboard/` +> 목표: 파일 분리 + 모바일(344px) 대응 + +--- + +## 1. 현재 상태 분석 + +### 1.1 파일 구조 + +``` +CEODashboard/ +├── CEODashboard.tsx # 1,648줄 ⚠️ 분리 필요 +├── components.tsx # 312줄 ✅ 적정 +├── types.ts # ~100줄 ✅ 적정 +├── sections/ +│ ├── index.ts +│ ├── TodayIssueSection.tsx # 73줄 ✅ +│ ├── DailyReportSection.tsx # 37줄 ✅ +│ ├── MonthlyExpenseSection.tsx # 38줄 ✅ +│ ├── CardManagementSection.tsx # ~50줄 ✅ +│ ├── EntertainmentSection.tsx # ~50줄 ✅ +│ ├── WelfareSection.tsx # ~50줄 ✅ +│ ├── ReceivableSection.tsx # ~50줄 ✅ +│ ├── DebtCollectionSection.tsx # ~50줄 ✅ +│ ├── VatSection.tsx # ~50줄 ✅ +│ └── CalendarSection.tsx # ~100줄 ✅ +├── modals/ +│ ├── ScheduleDetailModal.tsx # ~200줄 ✅ +│ └── DetailModal.tsx # ~300줄 ✅ +└── dialogs/ + └── DashboardSettingsDialog.tsx # ~200줄 ✅ +``` + +### 1.2 CEODashboard.tsx 내부 분석 (1,648줄) + +| 줄 범위 | 내용 | 줄 수 | 분리 대상 | +|---------|------|-------|----------| +| 1-26 | imports | 26 | - | +| 27-370 | mockData 객체 | **344** | ✅ 분리 | +| 371-748 | handleMonthlyExpenseCardClick (모달 config) | **378** | ✅ 분리 | +| 749-1019 | handleCardManagementCardClick (모달 config) | **271** | ✅ 분리 | +| 1020-1247 | handleEntertainmentCardClick (모달 config) | **228** | ✅ 분리 | +| 1248-1375 | handleWelfareCardClick (모달 config) | **128** | ✅ 분리 | +| 1376-1465 | handleVatClick (모달 config) | **90** | ✅ 분리 | +| 1466-1509 | 캘린더 관련 핸들러 | 44 | - | +| 1510-1648 | 컴포넌트 렌더링 | 139 | - | + +**분리 대상 총합**: ~1,439줄 (87%) +**분리 후 예상**: ~210줄 + +--- + +## 2. 분리 계획 + +### 2.1 목표 구조 + +``` +CEODashboard/ +├── CEODashboard.tsx # ~250줄 (컴포넌트 + 핸들러) +├── components.tsx # 312줄 (유지) +├── types.ts # ~100줄 (유지) +├── mockData.ts # 🆕 ~350줄 (목데이터) +├── modalConfigs/ # 🆕 모달 설정 분리 +│ ├── index.ts +│ ├── monthlyExpenseConfigs.ts # ~380줄 +│ ├── cardManagementConfigs.ts # ~280줄 +│ ├── entertainmentConfigs.ts # ~230줄 +│ ├── welfareConfigs.ts # ~130줄 +│ └── vatConfigs.ts # ~100줄 +├── sections/ # (유지) +├── modals/ # (유지) +└── dialogs/ # (유지) +``` + +### 2.2 분리 파일 상세 + +#### A. mockData.ts (신규) + +```typescript +// mockData.ts +import type { CEODashboardData } from './types'; + +export const mockData: CEODashboardData = { + todayIssue: [...], + dailyReport: {...}, + monthlyExpense: {...}, + cardManagement: {...}, + entertainment: {...}, + welfare: {...}, + receivable: {...}, + debtCollection: {...}, + vat: {...}, + calendarSchedules: [...], +}; +``` + +#### B. modalConfigs/index.ts (신규) + +```typescript +// modalConfigs/index.ts +export { getMonthlyExpenseModalConfig } from './monthlyExpenseConfigs'; +export { getCardManagementModalConfig } from './cardManagementConfigs'; +export { getEntertainmentModalConfig } from './entertainmentConfigs'; +export { getWelfareModalConfig } from './welfareConfigs'; +export { getVatModalConfig } from './vatConfigs'; +``` + +#### C. 개별 모달 config 파일 예시 + +```typescript +// modalConfigs/monthlyExpenseConfigs.ts +import type { DetailModalConfig } from '../types'; + +export function getMonthlyExpenseModalConfig(cardId: string): DetailModalConfig | null { + const configs: Record = { + me1: { title: '당월 매입 상세', ... }, + me2: { title: '당월 카드 상세', ... }, + me3: { title: '당월 발행어음 상세', ... }, + me4: { title: '당월 지출 예상 상세', ... }, + }; + return configs[cardId] || null; +} +``` + +#### D. CEODashboard.tsx (리팩토링 후) + +```typescript +// CEODashboard.tsx (리팩토링 후 ~250줄) +import { mockData } from './mockData'; +import { + getMonthlyExpenseModalConfig, + getCardManagementModalConfig, + getEntertainmentModalConfig, + getWelfareModalConfig, + getVatModalConfig, +} from './modalConfigs'; + +export function CEODashboard() { + // 상태 관리 + const [data] = useState(mockData); + const [detailModalConfig, setDetailModalConfig] = useState(null); + // ... + + // 간소화된 핸들러 + const handleMonthlyExpenseCardClick = useCallback((cardId: string) => { + const config = getMonthlyExpenseModalConfig(cardId); + if (config) { + setDetailModalConfig(config); + setIsDetailModalOpen(true); + } + }, []); + + // 렌더링 + return (...); +} +``` + +--- + +## 3. 모바일 대응 계획 + +### 3.1 적용 대상 컴포넌트 + +| 컴포넌트 | 현재 상태 | 변경 필요 | +|----------|----------|----------| +| TodayIssueSection | `grid-cols-2 md:grid-cols-4` | ✅ `grid-cols-1 xs:grid-cols-2 md:grid-cols-4` | +| DailyReportSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| MonthlyExpenseSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| CardManagementSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| EntertainmentSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| WelfareSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| ReceivableSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| DebtCollectionSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| VatSection | `grid-cols-2 md:grid-cols-4` | ✅ 동일 | +| AmountCardItem (공통) | 고정 텍스트 크기 | ✅ 반응형 텍스트 | +| IssueCardItem (공통) | 고정 텍스트 크기 | ✅ 반응형 텍스트 | +| PageHeader | 가로 배치 | ✅ 세로/가로 반응형 | + +### 3.2 components.tsx 변경 사항 + +#### AmountCardItem + +```tsx +// Before +

+ {formatCardAmount(card.amount)} +

+ +// After +

+ {formatCardAmount(card.amount)} +

+

+ {card.label} +

+``` + +#### IssueCardItem + +```tsx +// Before +

+ {typeof count === 'number' ? `${count}건` : count} +

+ +// After +

+ {typeof count === 'number' ? `${count}건` : count} +

+``` + +### 3.3 섹션 공통 변경 + +```tsx +// Before (모든 섹션) +
+ +// After +
+``` + +### 3.4 CardContent 패딩 + +```tsx +// Before + + +// After + +``` + +--- + +## 4. 실행 계획 + +### Phase 1: 파일 분리 (예상 30분) + +- [ ] **1.1** `mockData.ts` 생성 및 데이터 이동 +- [ ] **1.2** `modalConfigs/` 폴더 생성 +- [ ] **1.3** `monthlyExpenseConfigs.ts` 생성 +- [ ] **1.4** `cardManagementConfigs.ts` 생성 +- [ ] **1.5** `entertainmentConfigs.ts` 생성 +- [ ] **1.6** `welfareConfigs.ts` 생성 +- [ ] **1.7** `vatConfigs.ts` 생성 +- [ ] **1.8** `modalConfigs/index.ts` 생성 +- [ ] **1.9** `CEODashboard.tsx` 리팩토링 +- [ ] **1.10** import 정리 및 동작 확인 + +### Phase 2: 모바일 대응 (예상 30분) + +- [ ] **2.1** `components.tsx` - AmountCardItem 반응형 적용 +- [ ] **2.2** `components.tsx` - IssueCardItem 반응형 적용 +- [ ] **2.3** `sections/*.tsx` - 그리드 반응형 적용 (일괄) +- [ ] **2.4** `sections/*.tsx` - CardContent 패딩 반응형 적용 +- [ ] **2.5** PageHeader 반응형 확인 +- [ ] **2.6** 344px 테스트 및 미세 조정 + +### Phase 3: 검증 (예상 15분) + +- [ ] **3.1** 빌드 확인 요청 +- [ ] **3.2** 데스크탑(1280px) 동작 확인 +- [ ] **3.3** 태블릿(768px) 동작 확인 +- [ ] **3.4** 모바일(375px) 동작 확인 +- [ ] **3.5** Galaxy Fold(344px) 동작 확인 + +--- + +## 5. 예상 결과 + +### 5.1 파일 크기 변화 + +| 파일 | Before | After | +|------|--------|-------| +| CEODashboard.tsx | 1,648줄 | ~250줄 | +| mockData.ts | - | ~350줄 | +| modalConfigs/*.ts | - | ~1,100줄 (5개 파일) | + +### 5.2 장점 + +1. **유지보수성**: 각 파일이 단일 책임 원칙 준수 +2. **재사용성**: 모달 config를 다른 곳에서 재사용 가능 +3. **확장성**: 새 모달 추가 시 별도 파일로 분리 +4. **가독성**: 핵심 로직만 CEODashboard.tsx에 유지 +5. **API 전환 용이**: mockData.ts만 교체하면 됨 + +### 5.3 모바일 개선 효과 + +| 항목 | Before (344px) | After (344px) | +|------|----------------|---------------| +| 카드 배치 | 2열 (160px/카드) | 1열 (320px/카드) | +| 금액 표시 | 잘림 가능 | 완전 표시 | +| 라벨 표시 | 잘림 가능 | 줄바꿈/truncate | +| 패딩 | 과다 (24px) | 적정 (12px) | + +--- + +## 6. 참고 문서 + +- **모바일 대응 가이드**: `claudedocs/guides/[GUIDE] mobile-responsive-patterns.md` +- **기존 테스트 계획**: `claudedocs/[PLAN] mobile-overflow-testing.md` + +--- + +## 7. 의사결정 사항 + +### Q1: mockData를 별도 파일로? +- **결정**: ✅ 분리 +- **이유**: 향후 API 연동 시 교체 용이 + +### Q2: 모달 config를 폴더로? +- **결정**: ✅ 폴더로 분리 +- **이유**: 각 config가 100줄 이상, 단일 파일은 여전히 큼 + +### Q3: 모바일에서 1열 vs 2열? +- **결정**: 344px 이하 1열, 375px 이상 2열 +- **이유**: Galaxy Fold 160px 카드는 너무 좁음 + +--- + +## 8. 시작 조건 + +- [x] 계획서 작성 완료 +- [x] 모바일 가이드 작성 완료 +- [ ] 사용자 승인 + +--- + +> **다음 단계**: 계획 승인 후 Phase 1 (파일 분리) 시작 \ No newline at end of file diff --git a/claudedocs/guides/[GUIDE] mobile-responsive-patterns.md b/claudedocs/guides/[GUIDE] mobile-responsive-patterns.md new file mode 100644 index 00000000..f2fd8f99 --- /dev/null +++ b/claudedocs/guides/[GUIDE] mobile-responsive-patterns.md @@ -0,0 +1,538 @@ +# 모바일 반응형 패턴 가이드 + +> 작성일: 2026-01-10 +> 적용 범위: SAM 프로젝트 전체 +> 주요 대상 기기: Galaxy Z Fold 5 (접힌 상태 344px) + +--- + +## 1. 브레이크포인트 정의 + +### 1.1 Tailwind 기본 브레이크포인트 + +| 접두사 | 최소 너비 | 대상 기기 | +|--------|----------|----------| +| (기본) | 0px | Galaxy Fold 접힌 (344px) | +| `xs` | 375px | iPhone SE, 소형 모바일 | +| `sm` | 640px | 대형 모바일, 소형 태블릿 | +| `md` | 768px | 태블릿 | +| `lg` | 1024px | 소형 데스크탑 | +| `xl` | 1280px | 데스크탑 | +| `2xl` | 1536px | 대형 데스크탑 | + +### 1.2 커스텀 브레이크포인트 (tailwind.config.js) + +```javascript +// tailwind.config.js +module.exports = { + theme: { + screens: { + 'xs': '375px', // iPhone SE + 'sm': '640px', + 'md': '768px', + 'lg': '1024px', + 'xl': '1280px', + '2xl': '1536px', + // Galaxy Fold 전용 (선택적) + 'fold': '344px', + }, + }, +} +``` + +### 1.3 주요 테스트 뷰포트 + +| 기기 | 너비 | 높이 | 우선순위 | +|------|------|------|----------| +| Galaxy Z Fold 5 (접힌) | **344px** | 882px | 🔴 필수 | +| iPhone SE | 375px | 667px | 🔴 필수 | +| iPhone 14 Pro | 393px | 852px | 🟡 권장 | +| iPad Mini | 768px | 1024px | 🟡 권장 | +| Desktop | 1280px+ | - | 🟢 기본 | + +--- + +## 2. 공통 패턴별 해결책 + +### 2.1 그리드 레이아웃 + +#### 문제 +344px에서 `grid-cols-2`는 각 항목이 ~160px로 좁아져 텍스트 오버플로우 발생 + +#### 해결 패턴 + +**패턴 A: 1열 → 2열 → 4열 (권장)** +```tsx +// Before +
+ +// After - 344px에서 1열 +
+``` + +**패턴 B: 최소 너비 보장** +```tsx +// 카드 최소 너비 보장 + 자동 열 조정 +
+``` + +**패턴 C: Flex Wrap (항목 수 가변적일 때)** +```tsx +
+
+ {/* 카드 내용 */} +
+
+``` + +#### 적용 기준 +| 카드 개수 | 권장 패턴 | +|-----------|----------| +| 1-2개 | `grid-cols-1 xs:grid-cols-2` | +| 3-4개 | `grid-cols-1 xs:grid-cols-2 md:grid-cols-4` | +| 5개+ | `grid-cols-1 xs:grid-cols-2 md:grid-cols-3 lg:grid-cols-4` | + +--- + +### 2.2 테이블 반응형 + +#### 문제 +테이블이 344px 화면에서 가로 스크롤 없이 표시 불가 + +#### 해결 패턴 + +**패턴 A: 가로 스크롤 (기본)** +```tsx +
+ + {/* 테이블 내용 */} +
+
+``` + +**패턴 B: 카드형 변환 (복잡한 데이터)** +```tsx +{/* 데스크탑: 테이블 */} + + {/* 테이블 내용 */} +
+ +{/* 모바일: 카드 리스트 */} +
+ {data.map((item) => ( + + +
+ 거래처 + {item.vendor} +
+ {/* 추가 필드 */} +
+
+ ))} +
+``` + +**패턴 C: 컬럼 숨김 (우선순위 기반)** +```tsx +등록일 +수정일 +필수 컬럼 + +{item.createdAt} +{item.updatedAt} +{item.essential} +``` + +--- + +### 2.3 카드 컴포넌트 + +#### 문제 +카드 내 금액, 라벨이 좁은 화면에서 잘림 + +#### 해결 패턴 + +**패턴 A: 텍스트 크기 반응형** +```tsx +// Before +

30,500,000,000원

+ +// After +

30.5억원

+``` + +**패턴 B: 금액 포맷 함수 개선** +```typescript +// utils/format.ts +export const formatAmountResponsive = (amount: number, compact = false): string => { + if (compact || amount >= 100000000) { + // 억 단위 + const billion = amount / 100000000; + return billion >= 1 ? `${billion.toFixed(1)}억원` : formatAmount(amount); + } + if (amount >= 10000) { + // 만 단위 + const man = amount / 10000; + return `${man.toFixed(0)}만원`; + } + return new Intl.NumberFormat('ko-KR').format(amount) + '원'; +}; +``` + +**패턴 C: 라벨 줄바꿈 허용** +```tsx +// Before +

현금성 자산 합계

+ +// After +

현금성 자산 합계

+``` + +**패턴 D: Truncate + Tooltip** +```tsx +

+ {longLabel} +

+``` + +--- + +### 2.4 모달/다이얼로그 + +#### 문제 +모달이 344px 화면에서 좌우 여백 없이 꽉 차거나 넘침 + +#### 해결 패턴 + +**패턴 A: 최대 너비 반응형** +```tsx +// Before + + +// After + +``` + +**패턴 B: 전체 화면 모달 (복잡한 내용)** +```tsx + +``` + +**패턴 C: 모달 내부 스크롤** +```tsx + + + {/* 헤더 */} + +
+ {/* 스크롤 가능한 내용 */} +
+ + {/* 푸터 */} + +
+``` + +--- + +### 2.5 버튼 그룹 + +#### 문제 +여러 버튼이 가로로 나열될 때 344px에서 넘침 + +#### 해결 패턴 + +**패턴 A: Flex Wrap** +```tsx +// Before +
+ + + +
+ +// After +
+ + + +
+``` + +**패턴 B: 세로 배치 (모바일)** +```tsx +
+ + +
+``` + +**패턴 C: 아이콘 전용 (극소 화면)** +```tsx + +``` + +--- + +### 2.6 긴 텍스트 처리 + +#### 문제 +긴 제목, 설명, 메시지가 좁은 화면에서 레이아웃 깨짐 + +#### 해결 패턴 + +**패턴 A: Truncate (한 줄)** +```tsx +

+ {title} +

+``` + +**패턴 B: Line Clamp (여러 줄)** +```tsx +

+ {description} +

+``` + +**패턴 C: Break Keep (한글 단어 단위)** +```tsx +

+ 가지급금 인정이자 4.6%, 법인세 및 연말정산 시 대표자 종합세 가중 주의 +

+``` + +**패턴 D: 반응형 텍스트 크기** +```tsx +

+ {title} +

+``` + +--- + +### 2.7 헤더/네비게이션 + +#### 문제 +페이지 헤더의 타이틀과 액션 버튼이 충돌 + +#### 해결 패턴 + +**패턴 A: 세로 배치 (모바일)** +```tsx +
+
+

{title}

+

{description}

+
+
+ +
+
+``` + +**패턴 B: 아이콘 버튼 (극소 화면)** +```tsx + +``` + +--- + +### 2.8 패딩/마진 반응형 + +#### 문제 +데스크탑용 패딩이 모바일에서 공간 낭비 + +#### 해결 패턴 + +```tsx +// Before +
+ +// After +
+ +// 카드 내부 + +``` + +--- + +## 3. Tailwind 유틸리티 클래스 모음 + +### 3.1 자주 사용하는 반응형 패턴 + +```css +/* 그리드 */ +.grid-responsive-1-2-4: grid-cols-1 xs:grid-cols-2 md:grid-cols-4 +.grid-responsive-1-2-3: grid-cols-1 xs:grid-cols-2 md:grid-cols-3 +.grid-responsive-1-3: grid-cols-1 md:grid-cols-3 + +/* 텍스트 */ +.text-responsive-sm: text-xs xs:text-sm +.text-responsive-base: text-sm xs:text-base +.text-responsive-lg: text-base xs:text-lg md:text-xl +.text-responsive-xl: text-lg xs:text-xl md:text-2xl +.text-responsive-2xl: text-xl xs:text-2xl md:text-3xl + +/* 패딩 */ +.p-responsive: p-3 xs:p-4 md:p-6 +.px-responsive: px-3 xs:px-4 md:px-6 +.py-responsive: py-3 xs:py-4 md:py-6 + +/* 갭 */ +.gap-responsive: gap-2 xs:gap-3 md:gap-4 + +/* Flex 방향 */ +.flex-col-to-row: flex-col xs:flex-row +``` + +### 3.2 커스텀 유틸리티 (globals.css) + +```css +/* globals.css */ +@layer utilities { + .grid-responsive-cards { + @apply grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4; + } + + .text-amount { + @apply text-xl xs:text-2xl md:text-3xl font-bold; + } + + .card-padding { + @apply p-3 xs:p-4 md:p-6; + } + + .section-padding { + @apply p-4 xs:p-5 md:p-6; + } +} +``` + +--- + +## 4. 적용 체크리스트 + +### 4.1 페이지 단위 체크리스트 + +```markdown +## 페이지: [페이지명] +테스트 뷰포트: 344px (Galaxy Fold) + +### 레이아웃 +- [ ] 헤더 타이틀/액션 버튼 충돌 없음 +- [ ] 그리드 카드 오버플로우 없음 +- [ ] 사이드바 접힘 상태 정상 + +### 텍스트 +- [ ] 제목 텍스트 잘림/줄바꿈 정상 +- [ ] 금액 표시 가독성 확보 +- [ ] 라벨 텍스트 truncate 또는 줄바꿈 + +### 테이블 +- [ ] 가로 스크롤 정상 동작 +- [ ] 필수 컬럼 표시 확인 +- [ ] 체크박스/액션 버튼 접근 가능 + +### 카드 +- [ ] 카드 내용 오버플로우 없음 +- [ ] 터치 영역 충분 (최소 44px) +- [ ] 카드 간 간격 적절 + +### 모달 +- [ ] 화면 내 완전히 표시 +- [ ] 닫기 버튼 접근 가능 +- [ ] 내부 스크롤 정상 + +### 버튼 +- [ ] 버튼 그룹 wrap 정상 +- [ ] 터치 영역 충분 +- [ ] 아이콘/텍스트 가독성 +``` + +### 4.2 컴포넌트 단위 체크리스트 + +```markdown +## 컴포넌트: [컴포넌트명] + +### 필수 확인 +- [ ] min-width 고정값 없음 또는 반응형 처리 +- [ ] whitespace-nowrap 사용 시 truncate 동반 +- [ ] grid-cols-N 사용 시 모바일 breakpoint 추가 +- [ ] 패딩/마진 반응형 적용 + +### 권장 확인 +- [ ] 텍스트 크기 반응형 +- [ ] 버튼 크기 반응형 +- [ ] 아이콘 크기 반응형 +``` + +--- + +## 5. 적용 사례 + +### 5.1 CEO 대시보드 적용 예정 + +**현재 문제점**: +- `grid-cols-2 md:grid-cols-4` → 344px에서 카드당 ~160px +- 금액 "3,050,000,000원" 표시 → 잘림 +- "현금성 자산 합계" 라벨 → 잘림 + +**적용 계획**: +1. 그리드: `grid-cols-1 xs:grid-cols-2 md:grid-cols-4` +2. 금액: `formatAmountResponsive()` 함수 사용 (억 단위) +3. 라벨: `break-keep` 또는 `truncate` +4. 카드 패딩: `p-3 xs:p-4 md:p-6` +5. 헤더 버튼: 아이콘 전용 옵션 + +**상세 계획**: `[PLAN] ceo-dashboard-refactoring.md` 참조 + +--- + +## 6. 테스트 방법 + +### 6.1 Chrome DevTools 설정 + +1. DevTools 열기 (F12) +2. Device Toolbar (Ctrl+Shift+M) +3. Edit → Add custom device: + - Name: `Galaxy Z Fold 5 (Folded)` + - Width: `344` + - Height: `882` + - Device pixel ratio: `3` + - User agent: Mobile + +### 6.2 권장 테스트 순서 + +1. **344px**: 최소 지원 너비 (Galaxy Fold) +2. **375px**: iPhone SE +3. **768px**: 태블릿 +4. **1280px**: 데스크탑 + +### 6.3 자동화 테스트 (Playwright) + +```typescript +// playwright.config.ts +const devices = [ + { name: 'Galaxy Fold', viewport: { width: 344, height: 882 } }, + { name: 'iPhone SE', viewport: { width: 375, height: 667 } }, + { name: 'iPad', viewport: { width: 768, height: 1024 } }, + { name: 'Desktop', viewport: { width: 1280, height: 800 } }, +]; +``` + +--- + +## 변경 이력 + +| 날짜 | 버전 | 변경 내용 | +|------|------|----------| +| 2026-01-10 | 1.0 | 초기 작성 | \ No newline at end of file diff --git a/src/app/[locale]/(protected)/board/[boardCode]/[postId]/page.tsx b/src/app/[locale]/(protected)/board/[boardCode]/[postId]/page.tsx index caa3b20c..54817992 100644 --- a/src/app/[locale]/(protected)/board/[boardCode]/[postId]/page.tsx +++ b/src/app/[locale]/(protected)/board/[boardCode]/[postId]/page.tsx @@ -7,7 +7,7 @@ import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { Loader2 } from 'lucide-react'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { BoardDetail } from '@/components/board/BoardDetail'; import { getPost } from '@/components/board/actions'; import type { Post, Comment } from '@/components/board/types'; @@ -60,11 +60,7 @@ export default function BoardDetailPage() { }, [boardCode, postId, router]); if (isLoading) { - return ( -
- -
- ); + return ; } if (!post) { diff --git a/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx index 18a6c9bc..ad093909 100644 --- a/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx @@ -3,6 +3,7 @@ import { useRouter, useParams } from 'next/navigation'; import { useState, useEffect, useCallback } from 'react'; import { Loader2 } from 'lucide-react'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { BoardForm } from '@/components/board/BoardManagement/BoardForm'; import { getBoardById, updateBoard } from '@/components/board/BoardManagement/actions'; import { forceRefreshMenus } from '@/lib/utils/menuRefresh'; @@ -64,11 +65,7 @@ export default function BoardEditPage() { // 로딩 상태 if (isLoading) { - return ( -
- -
- ); + return ; } // 에러 상태 diff --git a/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx b/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx index f8475ebd..b93de1cc 100644 --- a/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx @@ -3,6 +3,7 @@ import { useRouter, useParams } from 'next/navigation'; import { useState, useEffect, useCallback } from 'react'; import { Loader2 } from 'lucide-react'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { BoardDetail } from '@/components/board/BoardManagement/BoardDetail'; import { getBoardById, deleteBoard } from '@/components/board/BoardManagement/actions'; import { Button } from '@/components/ui/button'; @@ -74,11 +75,7 @@ export default function BoardDetailPage() { // 로딩 상태 if (isLoading) { - return ( -
- -
- ); + return ; } // 에러 상태 diff --git a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx index c8ebdab4..42211179 100644 --- a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx +++ b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx @@ -6,7 +6,8 @@ import { useState, useEffect } from 'react'; import { useRouter, useParams } from 'next/navigation'; -import { ArrowLeft, Save, MessageSquare, Loader2 } from 'lucide-react'; +import { ArrowLeft, Save, MessageSquare } from 'lucide-react'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -146,9 +147,7 @@ export default function DynamicBoardEditPage() { if (isLoading) { return ( -
- -
+
); } diff --git a/src/app/[locale]/(protected)/items/[id]/edit/page.tsx b/src/app/[locale]/(protected)/items/[id]/edit/page.tsx index 17db9e70..551edcbb 100644 --- a/src/app/[locale]/(protected)/items/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/items/[id]/edit/page.tsx @@ -13,7 +13,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation'; import DynamicItemForm from '@/components/items/DynamicItemForm'; import type { DynamicFormData, BOMLine } from '@/components/items/DynamicItemForm/types'; import type { ItemType } from '@/types/item'; -import { Loader2 } from 'lucide-react'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { isMaterialType, transformMaterialDataForSave, @@ -391,12 +391,7 @@ export default function EditItemPage() { // 로딩 상태 if (isLoading) { - return ( -
- -

품목 정보 로딩 중...

-
- ); + return ; } // 에러 상태 diff --git a/src/app/[locale]/(protected)/items/[id]/page.tsx b/src/app/[locale]/(protected)/items/[id]/page.tsx index c23d03e2..dca4368f 100644 --- a/src/app/[locale]/(protected)/items/[id]/page.tsx +++ b/src/app/[locale]/(protected)/items/[id]/page.tsx @@ -11,7 +11,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { notFound } from 'next/navigation'; import ItemDetailClient from '@/components/items/ItemDetailClient'; import type { ItemMaster, ItemType, ProductCategory, PartType, PartUsage } from '@/types/item'; -import { Loader2 } from 'lucide-react'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; // Materials 타입 (SM, RM, CS는 Material 테이블 사용) const MATERIAL_TYPES = ['SM', 'RM', 'CS']; @@ -255,12 +255,7 @@ export default function ItemDetailPage() { // 로딩 상태 if (isLoading) { - return ( -
- -

품목 정보 로딩 중...

-
- ); + return ; } // 에러 상태 diff --git a/src/app/[locale]/(protected)/production/dashboard/page.tsx b/src/app/[locale]/(protected)/production/dashboard/page.tsx index ffd86f86..ceb02e05 100644 --- a/src/app/[locale]/(protected)/production/dashboard/page.tsx +++ b/src/app/[locale]/(protected)/production/dashboard/page.tsx @@ -6,10 +6,11 @@ import { Suspense } from 'react'; import ProductionDashboard from '@/components/production/ProductionDashboard'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; export default function ProductionDashboardPage() { return ( - 로딩 중...
}> + }> ); diff --git a/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx b/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx index 6d654e8f..4183f961 100644 --- a/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import ItemForm from '@/components/items/ItemForm'; import type { ItemMaster } from '@/types/item'; import type { CreateItemFormData } from '@/lib/utils/validation'; @@ -189,11 +190,7 @@ export default function EditItemPage() { }; if (isLoading) { - return ( -
-
로딩 중...
-
- ); + return ; } if (!item) { diff --git a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx index a8bd08db..ce3b7591 100644 --- a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx @@ -6,6 +6,7 @@ import { use, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import ItemDetailClient from '@/components/items/ItemDetailClient'; import type { ItemMaster } from '@/types/item'; @@ -159,11 +160,7 @@ export default function ItemDetailPage({ }, [id]); if (isLoading) { - return ( -
-
로딩 중...
-
- ); + return ; } if (!item) { diff --git a/src/app/[locale]/(protected)/production/worker-screen/page.tsx b/src/app/[locale]/(protected)/production/worker-screen/page.tsx index 26ec4787..d7fce3bc 100644 --- a/src/app/[locale]/(protected)/production/worker-screen/page.tsx +++ b/src/app/[locale]/(protected)/production/worker-screen/page.tsx @@ -6,10 +6,11 @@ import { Suspense } from 'react'; import WorkerScreen from '@/components/production/WorkerScreen'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; export default function WorkerScreenPage() { return ( - 로딩 중...
}> + }> ); diff --git a/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx b/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx index 4d554c82..8d788c8a 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/Day1ChecklistPanel.tsx @@ -97,17 +97,17 @@ export function Day1ChecklistPanel({ return (
{/* 헤더 + 검색 */} -
-

점검표 항목

+
+

점검표 항목

{/* 검색 입력 */}
- + setSearchTerm(e.target.value)} - className="w-full pl-9 pr-8 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + className="w-full pl-8 sm:pl-9 pr-8 py-1.5 sm:py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> {searchTerm && ( -
+
{/* 문서 미리보기 영역 */} -
-
+
+
{/* Mock 문서 내용 */}
{/* 푸터 */} -
- +
+ 파일명: {document.fileName || '-'} - + 1 / 1 페이지
diff --git a/src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx b/src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx index 312ff3d3..212aa54b 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/DayTabs.tsx @@ -25,24 +25,27 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }: : 0; return ( -
+
{/* 탭 버튼 */} -
+
{/* 1일차 탭 */}
{/* 진행률 - 3줄 표시 */} -
+
{/* 전체 심사 진행률 */} -
- 전체 심사 -
+
+ + 전체 심사 + 전체 + +
{totalCompleted}/{totalItems} @@ -94,9 +103,12 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
{/* 1일차 진행률 */} -
- 1일차: 기준/매뉴얼 -
+
+ + 1일차: 기준/매뉴얼 + 1일차 + +
{day1Progress.completed}/{day1Progress.total} @@ -114,9 +126,12 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
{/* 2일차 진행률 */} -
- 2일차: 로트추적 -
+
+ + 2일차: 로트추적 + 2일차 + +
{day2Progress.completed}/{day2Progress.total} diff --git a/src/app/[locale]/(protected)/quality/qms/components/DocumentList.tsx b/src/app/[locale]/(protected)/quality/qms/components/DocumentList.tsx index a8b3d031..604219fc 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/DocumentList.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/DocumentList.tsx @@ -51,15 +51,15 @@ export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentL }; return ( -
-

+
+

관련 서류{' '} {routeCode && ( ({routeCode}) )}

-
+
{!routeCode ? (
수주루트를 선택해주세요. @@ -74,7 +74,7 @@ export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentL
handleDocClick(doc)} - className={`p-4 flex justify-between items-center transition-colors ${ + className={`p-3 sm:p-4 flex justify-between items-center transition-colors ${ hasItems ? 'cursor-pointer hover:bg-gray-50' : 'cursor-default opacity-60' } ${isExpanded ? 'bg-green-50' : 'bg-white'}`} > @@ -99,13 +99,13 @@ export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentL
{isExpanded && hasMultipleItems && ( -
-
+
+
{doc.items!.map((item) => (
handleItemClick(doc, item)} - className="flex items-center justify-between border border-gray-100 p-3 rounded cursor-pointer hover:bg-green-50 hover:border-green-200 transition-colors group" + className="flex items-center justify-between border border-gray-100 p-2 sm:p-3 rounded cursor-pointer hover:bg-green-50 hover:border-green-200 transition-colors group" >
{item.title}
diff --git a/src/app/[locale]/(protected)/quality/qms/components/Filters.tsx b/src/app/[locale]/(protected)/quality/qms/components/Filters.tsx index 2408ac22..1b890d2d 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/Filters.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/Filters.tsx @@ -24,13 +24,13 @@ export const Filters = ({ const years = [2025, 2024, 2023, 2022, 2021]; return ( -
+
{/* 상단: 년도/분기 선택 */} -
+
{/* Year Selection */}
년도 -
+
setNewColumnType(value)}> + setNewFieldOptions(e.target.value)} + value={optionsString} + onChange={(e) => handleSetNewFieldOptions(e.target.value)} placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)" />
@@ -404,18 +440,18 @@ export function FieldDialog({ )} {/* 조건부 표시 설정 - 모든 입력방식에서 사용 가능 */} - {(fieldInputMode === 'custom' || editingFieldId) && ( + {(isCustomMode || editingFieldId) && ( ConditionalFieldConfig[])) => void} tempConditionValue={tempConditionValue} setTempConditionValue={setTempConditionValue} newFieldKey={newFieldKey} - newFieldInputType={newFieldInputType} + newFieldInputType={newFieldInputType as InputType} selectedPage={selectedPage} selectedSectionForField={selectedSectionForField} editingFieldId={editingFieldId} @@ -438,7 +474,7 @@ export function FieldDialog({ }); setIsSubmitted(true); // 2025-11-28: field_key validation 추가 - const shouldValidate = fieldInputMode === 'custom' || editingFieldId; + const shouldValidate = isCustomMode || editingFieldId; console.log('[FieldDialog] 🔵 shouldValidate:', shouldValidate); if (shouldValidate && (isNameEmpty || isKeyEmpty || isKeyInvalid)) { console.log('[FieldDialog] ❌ 유효성 검사 실패로 return'); diff --git a/src/components/items/ItemMasterDataManagement/dialogs/FieldDrawer.tsx b/src/components/items/ItemMasterDataManagement/dialogs/FieldDrawer.tsx index 11913c7e..e2fdcc6f 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/FieldDrawer.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/FieldDrawer.tsx @@ -27,45 +27,74 @@ const INPUT_TYPE_OPTIONS = [ { value: 'textarea', label: '텍스트영역' } ]; +// 유연한 조건부 필드 타입 +interface FlexibleConditionField { + fieldId?: string; + fieldKey?: string; + fieldName?: string; + operator?: string; + value?: string; + expectedValue?: string; + logicOperator?: 'AND' | 'OR'; +} + +// 유연한 조건부 섹션 타입 +interface FlexibleConditionSection { + sectionId?: string; + sectionTitle?: string; + operator?: string; + value?: string; + logicOperator?: 'AND' | 'OR'; +} + +// 입력 모드 타입: 'new'/'existing' 또는 'custom'/'master' 모두 지원 +type FieldInputModeType = 'new' | 'existing' | 'custom' | 'master'; + interface FieldDrawerProps { isOpen: boolean; onOpenChange: (open: boolean) => void; editingFieldId: number | null; setEditingFieldId: (id: number | null) => void; - fieldInputMode: 'custom' | 'master'; - setFieldInputMode: (mode: 'custom' | 'master') => void; + // 'new'/'existing' 또는 'custom'/'master' 모두 지원 + fieldInputMode: FieldInputModeType; + setFieldInputMode: (mode: FieldInputModeType) => void; showMasterFieldList: boolean; setShowMasterFieldList: (show: boolean) => void; - selectedMasterFieldId: string; - setSelectedMasterFieldId: (id: string) => void; + // string 또는 number | null 모두 지원 + selectedMasterFieldId: string | number | null; + setSelectedMasterFieldId: (id: string | number | null) => void; textboxColumns: OptionColumn[]; setTextboxColumns: React.Dispatch>; newFieldConditionEnabled: boolean; setNewFieldConditionEnabled: (enabled: boolean) => void; newFieldConditionTargetType: 'field' | 'section'; setNewFieldConditionTargetType: (type: 'field' | 'section') => void; - newFieldConditionFields: Array<{ fieldKey: string; expectedValue: string }>; - setNewFieldConditionFields: React.Dispatch>>; - newFieldConditionSections: string[]; - setNewFieldConditionSections: React.Dispatch>; + // 유연한 조건부 필드 타입 + newFieldConditionFields: FlexibleConditionField[] | Array<{ fieldKey: string; expectedValue: string }>; + setNewFieldConditionFields: React.Dispatch> | React.Dispatch>>; + // 유연한 조건부 섹션 타입 + newFieldConditionSections: string[] | FlexibleConditionSection[]; + setNewFieldConditionSections: React.Dispatch> | React.Dispatch>; tempConditionValue: string; setTempConditionValue: (value: string) => void; newFieldName: string; setNewFieldName: (name: string) => void; newFieldKey: string; setNewFieldKey: (key: string) => void; - newFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'; - setNewFieldInputType: (type: any) => void; + // string 타입으로 유연하게 처리 + newFieldInputType: string; + setNewFieldInputType: (type: string) => void; newFieldRequired: boolean; setNewFieldRequired: (required: boolean) => void; newFieldDescription: string; setNewFieldDescription: (description: string) => void; - newFieldOptions: string; - setNewFieldOptions: (options: string) => void; + // string | string[] 모두 지원 + newFieldOptions: string | string[]; + setNewFieldOptions: ((options: string) => void) | React.Dispatch>; selectedSectionForField: ItemSection | null; selectedPage: ItemPage | null; itemMasterFields: ItemMasterField[]; - handleAddField: () => Promise; + handleAddField: () => void | Promise; setIsColumnDialogOpen: (open: boolean) => void; setEditingColumnId: (id: string | null) => void; setColumnName: (name: string) => void; @@ -116,6 +145,30 @@ export function FieldDrawer({ setColumnName, setColumnKey }: FieldDrawerProps) { + // 입력 모드 정규화: 'new' → 'custom', 'existing' → 'master' + const normalizedInputMode = + fieldInputMode === 'new' ? 'custom' : + fieldInputMode === 'existing' ? 'master' : + fieldInputMode; + + const isCustomMode = normalizedInputMode === 'custom'; + const isMasterMode = normalizedInputMode === 'master'; + + // 옵션을 문자열로 변환하여 처리 + const optionsString = Array.isArray(newFieldOptions) ? newFieldOptions.join(', ') : newFieldOptions; + + // setNewFieldOptions 래퍼 - union type 호환성 해결 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleSetNewFieldOptions = (options: string) => (setNewFieldOptions as any)(options); + + // setNewFieldConditionFields 래퍼 - union type 호환성 해결 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleSetNewFieldConditionFields = (updater: any) => (setNewFieldConditionFields as any)(updater); + + // setNewFieldConditionSections 래퍼 - union type 호환성 해결 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleSetNewFieldConditionSections = (updater: any) => (setNewFieldConditionSections as any)(updater); + const handleClose = () => { onOpenChange(false); setEditingFieldId(null); @@ -145,7 +198,7 @@ export function FieldDrawer({ {!editingFieldId && (