From 29e7b416151b40212c1209ed431d2fee61213521 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Thu, 8 Jan 2026 17:15:42 +0900 Subject: [PATCH] =?UTF-8?q?chore(WEB):=20=EB=8B=A4=EC=88=98=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?CEO=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CEO 대시보드 컴포넌트 추가 - AuthenticatedLayout 개선 - 각 모듈 actions.ts 에러 핸들링 개선 - API fetch-wrapper, refresh-token 로직 개선 - ReceivablesStatus 컴포넌트 업데이트 - globals.css 스타일 업데이트 - 기타 다수 컴포넌트 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ...IS-2026-01-07] permission-system-status.md | 240 ++ ...MPL-2026-01-07] ceo-dashboard-checklist.md | 435 +++ ...-08] dashboard-settings-popup-checklist.md | 130 + ...26-01-08] ceo-dashboard-session-context.md | 125 + claudedocs/_index.md | 6 +- ...[IMPL-2025-12-30] token-refresh-caching.md | 100 +- ...xtjs-security-update-and-migration-plan.md | 127 + .../(protected)/hr/attendance/page.tsx | 310 +- .../[locale]/(protected)/quality/qms/page.tsx | 10 +- src/app/api/auth/check/route.ts | 87 +- src/app/api/auth/refresh/route.ts | 41 +- src/app/globals.css | 15 +- .../accounting/BadDebtCollection/actions.ts | 11 + .../BankTransactionInquiry/actions.ts | 5 + .../accounting/BillManagement/actions.ts | 10 + .../CardTransactionInquiry/actions.ts | 5 + .../accounting/DailyReport/actions.ts | 6 + .../accounting/DepositManagement/actions.ts | 2 + .../ExpectedExpenseManagement/actions.ts | 12 + .../accounting/PurchaseManagement/actions.ts | 2 + .../accounting/ReceivablesStatus/actions.ts | 8 +- .../accounting/ReceivablesStatus/index.tsx | 34 +- .../accounting/ReceivablesStatus/types.ts | 1 - .../accounting/SalesManagement/actions.ts | 2 + .../accounting/VendorLedger/actions.ts | 7 + .../accounting/VendorManagement/actions.ts | 2 + .../WithdrawalManagement/actions.ts | 2 + .../approval/ApprovalBox/actions.ts | 6 + .../approval/DocumentCreate/actions.ts | 12 + src/components/approval/DraftBox/actions.ts | 8 + .../approval/ReferenceBox/actions.ts | 6 + src/components/attendance/actions.ts | 5 + .../board/BoardManagement/actions.ts | 10 + src/components/board/DynamicBoard/actions.ts | 11 + src/components/board/actions.ts | 8 + .../business/CEODashboard/CEODashboard.tsx | 605 ++++ .../business/CEODashboard/components.tsx | 312 ++ .../dialogs/DashboardSettingsDialog.tsx | 713 +++++ src/components/business/CEODashboard/index.ts | 2 + .../modals/ScheduleDetailModal.tsx | 289 ++ .../business/CEODashboard/modals/index.ts | 1 + .../CEODashboard/sections/CalendarSection.tsx | 277 ++ .../sections/CardManagementSection.tsx | 50 + .../sections/DailyReportSection.tsx | 40 + .../sections/DebtCollectionSection.tsx | 46 + .../sections/EntertainmentSection.tsx | 38 + .../sections/MonthlyExpenseSection.tsx | 38 + .../sections/ReceivableSection.tsx | 57 + .../sections/TodayIssueSection.tsx | 73 + .../CEODashboard/sections/VatSection.tsx | 38 + .../CEODashboard/sections/WelfareSection.tsx | 33 + .../business/CEODashboard/sections/index.ts | 10 + src/components/business/CEODashboard/types.ts | 257 ++ src/components/business/Dashboard.tsx | 32 +- src/components/business/Dashboard.tsx.backup2 | 25 + .../business/MainDashboard.tsx.backup | 2650 +++++++++++++++++ .../customer-center/shared/actions.ts | 11 + .../hr/AttendanceManagement/actions.ts | 2 + src/components/hr/CardManagement/actions.ts | 3 + .../hr/DepartmentManagement/actions.ts | 3 + .../hr/EmployeeManagement/actions.ts | 12 + src/components/hr/SalaryManagement/actions.ts | 2 + .../hr/VacationManagement/actions.ts | 19 + .../material/ReceivingManagement/actions.ts | 9 + .../material/StockStatus/actions.ts | 6 + .../outbound/ShipmentManagement/actions.ts | 13 + src/components/pricing/actions.ts | 9 + src/components/process-management/actions.ts | 13 + .../production/ProductionDashboard/actions.ts | 3 + .../production/WorkOrders/actions.ts | 15 + .../production/WorkResults/actions.ts | 10 + .../production/WorkerScreen/actions.ts | 9 + .../quality/InspectionManagement/actions.ts | 9 + src/components/quotes/actions.ts | 19 + src/components/reports/actions.ts | 5 + .../settings/AccountInfoManagement/actions.ts | 7 + .../settings/AccountManagement/actions.ts | 10 + .../AttendanceSettingsManagement/actions.ts | 5 + .../settings/CompanyInfoManagement/actions.ts | 5 + .../settings/LeavePolicyManagement/actions.ts | 4 + .../settings/NotificationSettings/actions.ts | 4 + .../PaymentHistoryManagement/actions.ts | 4 + .../settings/PermissionManagement/actions.ts | 15 + .../settings/PopupManagement/actions.ts | 8 + .../settings/RankManagement/actions.ts | 7 + .../SubscriptionManagement/actions.ts | 7 + .../settings/TitleManagement/actions.ts | 7 + .../WorkScheduleManagement/actions.ts | 4 + src/layouts/AuthenticatedLayout.tsx | 429 ++- src/lib/api/fetch-wrapper.ts | 14 +- src/lib/api/refresh-token.ts | 13 +- tsconfig.tsbuildinfo | 2 +- 92 files changed, 7695 insertions(+), 409 deletions(-) create mode 100644 claudedocs/[ANALYSIS-2026-01-07] permission-system-status.md create mode 100644 claudedocs/[IMPL-2026-01-07] ceo-dashboard-checklist.md create mode 100644 claudedocs/[IMPL-2026-01-08] dashboard-settings-popup-checklist.md create mode 100644 claudedocs/[NEXT-2026-01-08] ceo-dashboard-session-context.md create mode 100644 claudedocs/guides/[REF-2026-01-07] nextjs-security-update-and-migration-plan.md create mode 100644 src/components/business/CEODashboard/CEODashboard.tsx create mode 100644 src/components/business/CEODashboard/components.tsx create mode 100644 src/components/business/CEODashboard/dialogs/DashboardSettingsDialog.tsx create mode 100644 src/components/business/CEODashboard/index.ts create mode 100644 src/components/business/CEODashboard/modals/ScheduleDetailModal.tsx create mode 100644 src/components/business/CEODashboard/modals/index.ts create mode 100644 src/components/business/CEODashboard/sections/CalendarSection.tsx create mode 100644 src/components/business/CEODashboard/sections/CardManagementSection.tsx create mode 100644 src/components/business/CEODashboard/sections/DailyReportSection.tsx create mode 100644 src/components/business/CEODashboard/sections/DebtCollectionSection.tsx create mode 100644 src/components/business/CEODashboard/sections/EntertainmentSection.tsx create mode 100644 src/components/business/CEODashboard/sections/MonthlyExpenseSection.tsx create mode 100644 src/components/business/CEODashboard/sections/ReceivableSection.tsx create mode 100644 src/components/business/CEODashboard/sections/TodayIssueSection.tsx create mode 100644 src/components/business/CEODashboard/sections/VatSection.tsx create mode 100644 src/components/business/CEODashboard/sections/WelfareSection.tsx create mode 100644 src/components/business/CEODashboard/sections/index.ts create mode 100644 src/components/business/CEODashboard/types.ts create mode 100644 src/components/business/Dashboard.tsx.backup2 create mode 100644 src/components/business/MainDashboard.tsx.backup diff --git a/claudedocs/[ANALYSIS-2026-01-07] permission-system-status.md b/claudedocs/[ANALYSIS-2026-01-07] permission-system-status.md new file mode 100644 index 00000000..f5bc28ad --- /dev/null +++ b/claudedocs/[ANALYSIS-2026-01-07] permission-system-status.md @@ -0,0 +1,240 @@ +# 권한 관리 시스템 현황 분석 + +> 작성일: 2026-01-07 +> 목적: SAM 프로젝트 권한 시스템 현황 파악 및 향후 구현 계획 정리 + +--- + +## 1. 현재 상태 요약 + +| 구분 | 상태 | 설명 | +|------|------|------| +| 권한 설정 UI | ✅ 완성 | `/settings/permissions/[id]`에서 역할별 권한 설정 가능 | +| 백엔드 권한 API | ✅ 존재 | 권한 매트릭스 조회/설정 API 구현됨 | +| 백엔드 API 권한 체크 | ⚠️ 구조만 있음 | 미들웨어 존재하나 라우트에 미적용 | +| 프론트 권한 체크 | ❌ 미구현 | 권한 매트릭스 조회 및 UI 제어 로직 없음 | + +--- + +## 2. 권한 타입 (7가지) + +| 권한 | 영문 | 적용 대상 | +|------|------|----------| +| 조회 | `view` | 페이지 접근 | +| 생성 | `create` | 등록/추가 버튼 | +| 수정 | `update` | 수정 버튼 | +| 삭제 | `delete` | 삭제 버튼 | +| 승인 | `approve` | 승인/반려 버튼 | +| 내보내기 | `export` | Excel 다운로드 등 | +| 관리 | `manage` | 관리자 전용 기능 | + +--- + +## 3. 백엔드 API 구조 + +### 3.1 로그인 API + +**엔드포인트**: `POST /api/v1/login` + +**응답 구조**: +```json +{ + "access_token": "...", + "refresh_token": "...", + "user": { "id": 1, "name": "..." }, + "menus": [...], + "roles": [...] +} +``` + +> ⚠️ **주의**: 로그인 응답에 **권한 매트릭스(permissions)는 포함되지 않음** + +### 3.2 권한 매트릭스 조회 API + +**사용자별 권한 조회**: +``` +GET /api/v1/permissions/users/{userId}/menu-matrix +``` + +**응답 구조**: +```json +{ + "permission_types": ["view", "create", "update", "delete", "approve", "export", "manage"], + "permissions": { + "1": { "view": true, "create": true, "update": false, ... }, + "2": { "view": true, "create": false, ... } + } +} +``` + +### 3.3 기타 권한 API + +| 엔드포인트 | 설명 | +|-----------|------| +| `GET /api/v1/permissions/departments/{dept_id}/menu-matrix` | 부서별 권한 매트릭스 | +| `GET /api/v1/permissions/roles/{role_id}/menu-matrix` | 역할별 권한 매트릭스 | +| `GET /api/v1/roles/{id}/permissions/matrix` | 역할 권한 매트릭스 (설정 UI용) | +| `POST /api/v1/roles/{id}/permissions/toggle` | 개별 권한 토글 | +| `POST /api/v1/roles/{id}/permissions/allow-all` | 전체 허용 | +| `POST /api/v1/roles/{id}/permissions/deny-all` | 전체 거부 | + +--- + +## 4. 백엔드 권한 체크 미들웨어 + +### 4.1 CheckPermission.php + +```php +// 권한 체크 로직 +if (! AccessService::allows($user, $perm, $tenantId, 'api')) { + return response()->json(['message' => '권한이 없습니다.'], 403); +} + +// 단, perm 미지정 라우트는 통과 (현재 정책) +if (! $perm && ! $permsAny) { + return $next($request); // ← 현재 모든 API가 여기로 통과 +} +``` + +### 4.2 PermMapper.php + +HTTP 메서드에 따라 액션 자동 매핑: + +| HTTP 메서드 | 권한 액션 | +|------------|----------| +| GET, HEAD | view | +| POST | create | +| PUT, PATCH | update | +| DELETE | delete | + +**권한 형식**: `menu:{menuId}.{action}` (예: `menu:1.view`) + +### 4.3 현재 상태 + +- 미들웨어 구조는 갖춰져 있음 +- **라우트에 `menu_id` 설정이 안 되어 있어 실제 권한 체크 미동작** +- 모든 API가 권한 체크 없이 통과 + +--- + +## 5. 프론트엔드 현재 상태 + +### 5.1 구현된 것 + +- 로그인 시 `menus`, `roles` 데이터 저장 (localStorage) +- 사이드바 메뉴 표시 (백엔드에서 필터링된 메뉴) +- 메뉴 폴링 (30초 주기) + +### 5.2 미구현 사항 + +- 권한 매트릭스 API 호출 +- 권한 데이터 저장 +- `usePermission` 훅 +- 페이지/버튼별 권한 체크 + +--- + +## 6. 향후 구현 계획 + +### 6.1 프론트엔드 (1단계 - UI 제어) + +``` +로그인 성공 + ↓ +/api/v1/permissions/users/{userId}/menu-matrix 호출 + ↓ +권한 매트릭스 저장 (Zustand/localStorage) + ↓ +usePermission 훅으로 권한 체크 + ↓ +버튼/기능 숨김/비활성화 +``` + +**usePermission 훅 예시**: +```typescript +// 사용법 +const { canView, canCreate, canUpdate, canDelete } = usePermission('판매관리'); + +// 적용 +{canCreate && } +{canDelete && } +``` + +**환경 변수 플래그**: +```env +NEXT_PUBLIC_ENABLE_AUTHORIZATION=false # 개발 중에는 비활성화 +``` + +### 6.2 백엔드 (2단계 - API 보안) + +라우트에 `menu_id` 설정하여 API 레벨 권한 체크 활성화: + +```php +// 예시: routes/api.php +Route::get('/orders', [OrderController::class, 'index']) + ->defaults('menu_id', 5); // 판매관리 메뉴 ID + +Route::post('/orders', [OrderController::class, 'store']) + ->defaults('menu_id', 5); // POST → create 권한 자동 체크 +``` + +--- + +## 7. 보안 고려사항 + +### 7.1 현재 취약점 + +- 프론트에서만 UI 숨기면 개발자 도구로 우회 가능 +- 직접 API 호출 시 권한 없이도 작업 가능 + +### 7.2 권장 구조 (이중 보안) + +``` +프론트엔드: UI 컨트롤 (UX 향상) + ↓ +백엔드: API 권한 체크 (실제 보안) + ↓ +권한 없으면 403 반환 +``` + +--- + +## 8. 관련 파일 경로 + +### 프론트엔드 (sam-react-prod) + +| 파일 | 설명 | +|------|------| +| `src/app/[locale]/(protected)/settings/permissions/` | 권한 설정 페이지 | +| `src/components/settings/PermissionManagement/` | 권한 관리 컴포넌트 | +| `src/layouts/AuthenticatedLayout.tsx` | 메뉴 표시 레이아웃 | +| `src/middleware.ts` | 인증 체크 (권한 체크 없음) | + +### 백엔드 (sam-api) + +| 파일 | 설명 | +|------|------| +| `app/Http/Controllers/Api/V1/PermissionController.php` | 권한 매트릭스 API | +| `app/Http/Controllers/Api/V1/RolePermissionController.php` | 역할 권한 API | +| `app/Http/Middleware/CheckPermission.php` | 권한 체크 미들웨어 | +| `app/Http/Middleware/PermMapper.php` | HTTP → 액션 매핑 | +| `app/Services/Authz/AccessService.php` | 권한 판정 서비스 | + +--- + +## 9. 결론 + +**현재**: 권한 설정은 가능하지만, 프론트/백엔드 모두 권한 체크 미적용 + +**1단계 (프론트)**: +- 로그인 후 권한 매트릭스 API 호출 +- usePermission 훅으로 UI 제어 +- 환경 변수로 개발 중 비활성화 + +**2단계 (백엔드)**: +- 라우트에 menu_id 설정 +- API 레벨 권한 체크 활성화 + +--- + +*이 문서는 권한 시스템 구현 시 참고용으로 작성되었습니다.* \ No newline at end of file diff --git a/claudedocs/[IMPL-2026-01-07] ceo-dashboard-checklist.md b/claudedocs/[IMPL-2026-01-07] ceo-dashboard-checklist.md new file mode 100644 index 00000000..59e74f6b --- /dev/null +++ b/claudedocs/[IMPL-2026-01-07] ceo-dashboard-checklist.md @@ -0,0 +1,435 @@ +# [IMPL-2026-01-07] 대표님 전용 대시보드 구현 + +## 프로젝트 개요 + +| 항목 | 내용 | +|------|------| +| 작업명 | 대표님 전용 대시보드 (CEO Dashboard) | +| 기준 페이지 | `/reports/comprehensive-analysis` (종합분석) | +| 대상 페이지 | `/dashboard` (대시보드) | +| 기존 대시보드 처리 | 백업 후 새 대시보드로 교체 | +| 공통 컴포넌트 활용 | `ScheduleCalendar` (달력) | + +--- + +## 작업 범위 + +### Phase 1: 본 화면 구현 (현재 작업) ✅ 완료 +- [x] 스크린샷 분석 및 계획서 작성 +- [x] 기존 Dashboard 컴포넌트 백업 +- [x] CEO Dashboard 컴포넌트 생성 +- [x] 각 섹션별 컴포넌트 구현 (11개 섹션) + +### Phase 2: 팝업/상세 화면 구현 (추후 작업) +- [ ] 항목 설정 팝업 +- [ ] 일일 일보 정보 팝업 +- [ ] 해당월 예상 지출 상세 팝업 +- [ ] 납부세액 내역 상세 팝업 +- [ ] 일정 상세 팝업 +- [ ] 기타 상세 팝업들 + +--- + +## 페이지 구조 (스크린샷 기준) + +### 섹션 1: 대시보드 헤더 (Page 31 상단) +``` +┌─────────────────────────────────────────────────────────┐ +│ LOGO 대시보드 - 전체 현황을 조회합니다. [항목 설정] │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 항목 설정 버튼 | 우측 상단 | 대시보드 항목 설정 팝업 표시 | + +--- + +### 섹션 2: 오늘의 이슈 (Page 31) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 오늘의 이슈 │ +├──────────┬──────────┬──────────┬──────────────────────┤ +│ 수주 │ 채권 추심 │ 반전 재고 │ 제규 신고 │ +│ 3건 │ 3건 │ 3건 │ 부가세 신고 D-15 │ +├──────────┼──────────┼──────────┼──────────────────────┤ +│ 신규업체 │ 연차 │ 발주 │ 결재 요청 │ +│ 등록 3건│ 3건 │ 3건 │ 3건 │ +└──────────┴──────────┴──────────┴──────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 수주 | 수주 건수 | 수주 관리 화면 이동 | +| 채권 추심 | 채권 추심 건수 | 채권 추심 관리 화면 이동 | +| 반전 재고 | 빨간색 강조 (위험) | 재고 관리 화면 이동 | +| 제규 신고 | 부가세 신고 D-day | 세무 관리 화면 이동 | +| 신규 업체 등록 | 신규 업체 건수 | 업체 관리 화면 이동 | +| 연차 | 연차 신청 건수 | 연차 관리 화면 이동 | +| 발주 | 발주 건수 | 발주 관리 화면 이동 | +| 결재 요청 | 결재 대기 건수 | 결재 관리 화면 이동 | + +--- + +### 섹션 3: 일일 일보 (Page 31) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 일일 일보 2026년 1월 5일 월요일 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 입금/자산 │ 전월 매출 │ (지표3) │ (지표4) │ +│ 30.5억원 │ $11,123,000 │ 10.2억원 │ 3.5억원 │ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ⚠️ 최근 7일 평균 대비 3배 이상으로 입금이 발생했습니다. │ +│ ⚠️ 102만원이 감지됐습니다... (이상거래 감지) │ +│ ℹ️ 현금성 자산이 300건전환입니다. 월 운영비와 비용보다... │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 일일 일보 영역 전체 | 오늘 날짜 기준 일보 | 일일 일보 정보 팝업 표시 | + +--- + +### 섹션 4: 당월 예상 지출 내역 (Page 32) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 당월 예상 지출 내역 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 미청산가지급금│ 이달 예상 │ 전달 대비 │ 차이 │ +│ 30.5억원 │30,123,000원 │30,123,000원 │ 3.5억원 │ +│ 전달14%,+5% │ │ │ │ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ⚠️ 이번 달 예상 지출이 전달 해당 15% 증가했습니다... │ +│ ⚠️ 이번 달 예상 지출이 예상 12% 초과했습니다... │ +│ ✅ 이번 달 예상 지출이 전달 대비 8% 감소했습니다... │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 가지급금 | 미청산 가지급금 | 가지급금 관리 화면 이동 | +| 미청산 가지급금 | 대상 금액 | 미청산 가지급금 상세 화면 이동 | +| 해당월 예상 지출 | 지출 상세 | 해당월 예상 지출 상세 팝업 표시 | + +--- + +### 섹션 5: 카드/가지급금 관리 (Page 32) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 카드/가지급금 관리 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 해당달 대상 │ 가지급금 │ 미정산 │ 총잔액 │ +│30,123,000원 │ 3.5억원 │3,123,000원 │3,123,000원│ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ⚠️ 법인카드 사용 총 85만원이 가지급금으로 전환됐습니다... │ +│ ⚠️ 전 가지급금 1,520만원은 4.6%, 연 약 70만원의 인정이자...│ +│ ⚠️ 상품권/귀금속 등 현대비 불인정 항목 매입 건이 있습니다 │ +│ ℹ️ 주말 카드 사용 총 100만원 중 결과 지의... │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 법인카드 예상 가능 영역 | 카드 사용 현황 | 법인카드 관리 화면 이동 | + +--- + +### 섹션 6: 접대비 현황 (Page 32~33) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 접대비 현황 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 접대비 한도 │ 접대비 사용액 │ 한도 잔액 │ 기타 │ +│ 305.3억원 │40,123,000원 │30,123,000원 │10,000,000원│ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ✅ 접대비 사용 총 2,400만원 중 / 한도 4,000만원 (60%)... │ +│ ⚠️ 접대비 85% 도달. 연내 한도 600만원 잔액입니다... │ +│ ❌ 접대비 한도 초과 320만원 발생. 손금불산입되어... │ +│ ℹ️ 접대비 사용 총 3건(45만원)이 거래처 한도 누락... │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 접대비 영역 | 접대비 현황 | 해당월 예상 지출 상세 팝업 표시 | + +--- + +### 섹션 7: 복리후생비 현황 (Page 33) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 복리후생비 현황 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 총 복리후생비│누적 사용 │ 잠정 사용액 │ 잠정 한도 │ +│30,123,000원 │10,123,000원 │ 5,123,000원 │5,123,000원│ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ✅ 1인당 월 복리후생비 18만원. 업계 평균 내 정상 운영... │ +│ ⚠️ 식대가 월 25만원으로 비과세 한도 초과... │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +### 섹션 8: 미수금 현황 (Page 33) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 미수금 현황 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 누계 미수금 │ 30일 초과 │ 60일 초과 │ 90일 초과 │ +│30,123,000원 │10,123,000원 │ 3,123,000원 │2,123,000원│ +│매출:6,012만 │매출:6,012만 │매출:6,012만 │매출:6,012만│ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ❌ 90일 이상 장기 미수금 3건(2,500만원) 발생. 회수조치... │ +│ ⚠️ (주)대한전자 미수금 4,500만원으로 전체의 35%... │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 미수금 현황 목록 | 미수금 상세 | 미수금 상세 화면으로 이동 (1,2차 표시) | + +--- + +### 섹션 9: 채권추심 현황 (Page 34) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 채권추심 현황 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 총 채권 │ 추심 진행 │ 이달(?) │ 미회수(?) │ +│ 3.5억원 │30,123,000원 │ 3,123,000원 │ 2.8억원 │ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ℹ️ (주)대한전자 건 지급명령 신청 완료. 법원 결정까지... │ +│ ⚠️ (주)삼성테크 건 회수 불가 판정. 대손 처리 검토... │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 채권추심 현황 확록 | 채권 추심 목록 | 미상대금 수심관리 화면으로 이동 | + +--- + +### 섹션 10: 부가세 현황 (Page 34~35) +``` +┌─────────────────────────────────────────────────────────┐ +│ 🔴 부가세 현황 │ +├──────────────┬──────────────┬──────────────┬───────────┤ +│ 예상 납부세액 │ 예상 납부세액 │ 금액 │ 건수 │ +│ 30.5억원 │ 20.5억원 │ 1.1억원 │ 3건 │ +└──────────────┴──────────────┴──────────────┴───────────┘ +│ ⚠️ 2026년 1기 예정신고 기한, 예상 환급세액은 5,200... │ +│ ⚠️ 2026년 1기 예정신고 기한, 예상 납부세액은 118,100... │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 부가세 현황 확록 | 납부세액 내역 | 해당 납부세액 내역 상세 팝업 표시 | + +--- + +### 섹션 11: 캘린더 (Page 34~35) +``` +┌─────────────────────────────────────────────────────────┐ +│ < 2026년 1월 > [일정추가] [일우 월일요] │ +│ [전체▼] [발주▼] [사업▼] │ +├─────────────────────────────────────────────────────────┤ +│ 일 월 화 수 목 금 토 │ +│ 1 2 3 4 5 │ +│ 6 7 8 9 10 11 12 ← 6일 선택 (주황색) │ +│ 13 14 15 16 17 18 19 토/일 배경 노란색 │ +│ 20 21 22 23 24 25 26 │ +│ 27 28 29 30 31 │ +├─────────────────────────────────────────────────────────┤ +│ 1월 6일 화요일 총 4건 │ +├─────────────────────────────────────────────────────────┤ +│ ● 제목: 부서세 ✏️ │ +│ 기간: 2026-01-01~01-06 │ +│ 시간: 09:00 ~ 12:00 │ +├─────────────────────────────────────────────────────────┤ +│ ● 제목: 회의 │ +│ 기간: 2026-01-01~01-07 │ +│ 시간: 전일 │ +├─────────────────────────────────────────────────────────┤ +│ ● 제목: 1,123 │ +└─────────────────────────────────────────────────────────┘ +``` + +| 요소 | 설명 | 클릭 동작 | +|------|------|----------| +| 일정추가 버튼 | 일정 추가 | (미정) | +| 일우 월일요 버튼 | 일정/다음달 스케쥴 표시 | 일정/다음달 스케쥴 토글 | +| 필터 셀렉트 | 전체, 발주, 사업 등 | 일정 유형 필터링 (다중선택) | +| 날짜 클릭 | 해당 날짜 선택 | 선택 날짜 일정 목록 표시 | +| 일정 항목 | 개별 일정 | 일정 상세 팝업 표시 | +| 수정 아이콘 (✏️) | 일정 수정 | 일정 수정 화면으로 이동 | + +**달력 스타일:** +- 토요일/일요일: 배경 노란색 +- 선택된 날짜: 배경 주황색 +- 이전/다음 달: 이전달/다음달 이동 + +--- + +## 체크리스트 + +### 1. 사전 준비 ✅ +- [x] 기존 Dashboard 컴포넌트 백업 (`Dashboard.tsx.backup2`) +- [x] 기존 MainDashboard 컴포넌트 백업 (`MainDashboard.tsx.backup`) +- [x] CEO Dashboard 디렉토리 구조 생성 + +### 2. 컴포넌트 구조 생성 +``` +src/components/business/CEODashboard/ +├── index.tsx # 메인 컴포넌트 (export) +├── CEODashboard.tsx # 메인 레이아웃 +├── types.ts # 타입 정의 +├── actions.ts # Server Actions +├── sections/ +│ ├── DashboardHeader.tsx # 헤더 (항목 설정 버튼) +│ ├── TodayIssueSection.tsx # 오늘의 이슈 +│ ├── DailyReportSection.tsx # 일일 일보 +│ ├── MonthlyExpenseSection.tsx # 당월 예상 지출 내역 +│ ├── CardManagementSection.tsx # 카드/가지급금 관리 +│ ├── EntertainmentSection.tsx # 접대비 현황 +│ ├── WelfareSection.tsx # 복리후생비 현황 +│ ├── ReceivableSection.tsx # 미수금 현황 +│ ├── DebtCollectionSection.tsx # 채권추심 현황 +│ ├── VatSection.tsx # 부가세 현황 +│ └── CalendarSection.tsx # 캘린더 +└── dialogs/ # Phase 2에서 구현 + ├── ItemSettingDialog.tsx # 항목 설정 팝업 + ├── DailyReportDialog.tsx # 일일 일보 정보 팝업 + └── ... +``` + +### 3. 섹션별 구현 체크리스트 ✅ + +#### 3.1 대시보드 헤더 ✅ +- [x] 로고 영역 +- [x] 제목 + 설명 +- [x] 항목 설정 버튼 +- [ ] 항목 설정 팝업 연동 (Phase 2) + +#### 3.2 오늘의 이슈 ✅ +- [x] 8개 이슈 카드 그리드 (4x2) +- [x] 각 카드 클릭 시 해당 화면 이동 +- [x] 반전 재고 빨간색 강조 +- [x] 제규 신고 D-day 표시 + +#### 3.3 일일 일보 ✅ +- [x] 날짜 표시 (년/월/일/요일) +- [x] 4개 지표 카드 +- [x] 체크포인트 메시지 (경고/정보) +- [ ] 클릭 시 일일 일보 팝업 (Phase 2) + +#### 3.4 당월 예상 지출 내역 ✅ +- [x] 4개 금액 카드 +- [x] 전월 대비 증감 표시 +- [x] 체크포인트 메시지 +- [ ] 클릭 시 상세 팝업 (Phase 2) + +#### 3.5 카드/가지급금 관리 ✅ +- [x] 4개 금액 카드 +- [x] 체크포인트 메시지 +- [x] 클릭 시 해당 화면 이동 + +#### 3.6 접대비 현황 ✅ +- [x] 4개 금액 카드 +- [x] 체크포인트 메시지 +- [ ] 클릭 시 상세 팝업 (Phase 2) + +#### 3.7 복리후생비 현황 ✅ +- [x] 4개 금액 카드 +- [x] 체크포인트 메시지 + +#### 3.8 미수금 현황 ✅ +- [x] 4개 금액 카드 (기간별 분류) +- [x] 매출/입금 서브 정보 +- [x] 체크포인트 메시지 +- [x] 클릭 시 미수금 상세 화면 이동 + +#### 3.9 채권추심 현황 ✅ +- [x] 4개 금액 카드 +- [x] 체크포인트 메시지 +- [x] 클릭 시 미상대금 수심관리 화면 이동 + +#### 3.10 부가세 현황 ✅ +- [x] 4개 금액 카드 +- [x] 체크포인트 메시지 +- [ ] 클릭 시 납부세액 내역 팝업 (Phase 2) + +#### 3.11 캘린더 ✅ +- [x] ScheduleCalendar 공통 컴포넌트 활용 +- [x] 일정추가 버튼 +- [x] 필터 셀렉트 (전체/발주/사업/회의/세금) +- [ ] 토/일 배경 노란색 스타일 커스터마이징 (추후) +- [ ] 선택 날짜 주황색 스타일 (추후) +- [x] 선택 날짜 일정 목록 표시 +- [ ] 일정 항목 클릭 시 상세 팝업 (Phase 2) +- [x] 수정 아이콘 클릭 시 수정 화면 이동 + +### 4. 대시보드 교체 ✅ +- [x] Dashboard.tsx에서 MainDashboard → CEODashboard로 교체 +- [x] 타입 체크 통과 + +--- + +## 연동 페이지 목록 (오늘의 이슈 클릭 시) + +| 이슈 항목 | 연동 페이지 | 경로 (예상) | +|----------|------------|------------| +| 수주 | 수주 관리 | `/sales/orders` | +| 채권 추심 | 채권 추심 관리 | `/accounting/debt-collection` | +| 반전 재고 | 재고 관리 | `/inventory/stock` | +| 제규 신고 | 세무 관리 | `/accounting/tax` | +| 신규 업체 등록 | 업체 관리 | `/partners/vendors` | +| 연차 | 연차 관리 | `/hr/vacation` | +| 발주 | 발주 관리 | `/purchase/orders` | +| 결재 요청 | 결재 관리 | `/approval/pending` | + +--- + +## 팝업 목록 (Phase 2에서 구현) + +| 팝업 이름 | 트리거 | 내용 | +|----------|--------|------| +| 항목 설정 팝업 | 항목 설정 버튼 클릭 | 대시보드 표시 항목 설정 | +| 일일 일보 정보 팝업 | 일일 일보 영역 클릭 | 일일 일보 상세 정보 | +| 해당월 예상 지출 상세 팝업 | 당월 예상 지출 클릭 | 지출 상세 내역 | +| 납부세액 내역 상세 팝업 | 부가세 현황 클릭 | 납부세액 상세 내역 | +| 일정 상세 팝업 | 일정 항목 클릭 | 일정 상세 정보 | + +--- + +## 참고 사항 + +### 기존 컴포넌트 재활용 +- `ComprehensiveAnalysis`: 많은 섹션 패턴 참고 가능 + - `SectionTitle`: 섹션 제목 컴포넌트 + - `AmountCardItem`: 금액 카드 컴포넌트 + - `CheckPointItem`: 체크포인트 메시지 컴포넌트 +- `ScheduleCalendar`: 달력 공통 컴포넌트 + - 월/주 뷰 지원 + - 이벤트/뱃지 표시 + - 커스터마이징 가능 + +### 스타일 가이드 +- 빨간색 강조: 위험/긴급 항목 (반전 재고 등) +- 주황색: 선택된 날짜 +- 노란색 배경: 토요일/일요일 +- 체크포인트 아이콘: + - ✅ 성공 (초록) + - ⚠️ 경고 (주황) + - ❌ 에러 (빨강) + - ℹ️ 정보 (파랑) + +--- + +## 변경 이력 + +| 날짜 | 작업 내용 | 상태 | +|------|----------|------| +| 2026-01-07 | 계획서 작성 | 완료 | +| 2026-01-07 | Phase 1 본 화면 구현 완료 (11개 섹션) | 완료 | \ No newline at end of file diff --git a/claudedocs/[IMPL-2026-01-08] dashboard-settings-popup-checklist.md b/claudedocs/[IMPL-2026-01-08] dashboard-settings-popup-checklist.md new file mode 100644 index 00000000..7242cb3d --- /dev/null +++ b/claudedocs/[IMPL-2026-01-08] dashboard-settings-popup-checklist.md @@ -0,0 +1,130 @@ +# 대시보드 항목 설정 팝업 구현 계획서 + +## 개요 +- **화면명**: 항목 설정_대시보드 팝업 +- **목적**: CEO 대시보드에 표시할 섹션들을 사용자가 ON/OFF로 선택할 수 있는 설정 팝업 +- **경로**: 대시보드 > 항목 설정 버튼 클릭 시 팝업 표시 + +## 기능 요구사항 + +### 1. 기본 구조 +- 모달/다이얼로그 형태의 팝업 +- 헤더: "항목 설정" 제목 + X 닫기 버튼 +- 푸터: 취소 | 저장 버튼 + +### 2. 섹션별 ON/OFF 토글 + +#### 오늘의 이슈 (전체 토글 + 개별 토글) +| 항목 | 기본값 | 비고 | +|------|--------|------| +| 오늘의 이슈 (전체) | ON | 빨간 배경 - 전체 ON/OFF | +| 수주 | ON | | +| 채권 추심 | ON | | +| 안전 재고 | ON | | +| 세금 신고 | OFF | | +| 신규 업체 등록 | OFF | | +| 연차 | ON | | +| 지각 | ON | | +| 결근 | OFF | | +| 발주 | OFF | | +| 결재 요청 | OFF | | + +#### 메인 섹션 토글 (접기/펼치기 가능) +| 섹션 | 기본값 | 하위 설정 | +|------|--------|----------| +| 일일 일보 | ON | - | +| 당월 예상 지출 내역 | ON | - | +| 카드/가지급금 관리 | ON | - | +| 접대비 현황 | ON | 접대비 한도 관리 (연간/분기), 기업 구분 | +| 복리후생비 현황 | ON | 복리후생비 한도 관리, 계산 방식, 금액 설정 | +| 미수금 현황 | ON | 미수금 상위 회사 현황 | +| 채권추심 현황 | ON | - | +| 부가세 현황 | ON | - | +| 캘린더 | ON | - | + +### 3. 상세 설정 옵션 + +#### 접대비 현황 하위 설정 +- 접대비 한도 관리: 연간 / 분기 선택 (드롭다운) +- 기업 구분: 기업 선택 (드롭다운) + 설명 버튼 + +#### 복리후생비 현황 하위 설정 +- 복리후생비 한도 관리: 연간 / 분기 선택 (드롭다운) +- 계산 방식: 직원당 정해 금액 방식 / 연봉 총액 X 비율 방식 (드롭다운) +- 직원당 정해 금액/월: 금액 입력 (계산 방식이 "직원당 정해 금액 방식"일 때) +- 비율: % 입력 (계산 방식이 "연봉 총액 X 비율 방식"일 때) +- 연간 복리후생비총액: 자동 계산 또는 직접 입력 + +### 4. 기업 구분 설명 패널 +- 1-2 버튼 클릭 시 기업 구분 기준 설명 펼침/접힘 +- 중소기업 판단 기준 설명 (자본총액 기준, 매출액 기준) +- 정보 제공용 (읽기 전용) + +### 5. 데이터 저장 +- localStorage 또는 API를 통한 설정 저장 +- 저장 버튼 클릭 시 설정 적용 및 대시보드 새로고침 +- 취소 버튼 클릭 시 변경사항 무시하고 팝업 닫기 + +--- + +## 구현 체크리스트 + +### Phase 1: 기본 팝업 구조 +- [x] 1.1 DashboardSettingsDialog 컴포넌트 생성 +- [x] 1.2 타입 정의 (DashboardSettings 인터페이스) +- [x] 1.3 기본 다이얼로그 UI 구현 (헤더, 푸터) +- [x] 1.4 CEODashboard에서 팝업 연결 + +### Phase 2: 오늘의 이슈 섹션 +- [x] 2.1 전체 토글 (빨간 배경) 구현 +- [x] 2.2 개별 항목 토글 목록 구현 +- [x] 2.3 전체 토글 연동 (전체 OFF 시 개별 모두 OFF) + +### Phase 3: 메인 섹션 토글 +- [x] 3.1 접기/펼치기 가능한 섹션 아코디언 구현 +- [x] 3.2 일일 일보 ~ 캘린더 섹션 토글 구현 +- [x] 3.3 섹션별 ON/OFF 상태 관리 + +### Phase 4: 상세 설정 옵션 +- [x] 4.1 접대비 현황 하위 설정 (한도 관리, 기업 구분) +- [x] 4.2 복리후생비 현황 하위 설정 (한도 관리, 계산 방식, 금액) +- [ ] 4.3 기업 구분 설명 패널 (펼침/접힘) - 기획서 확인 후 추가 구현 필요 + +### Phase 5: 데이터 연동 +- [x] 5.1 설정 상태 관리 (useState/useReducer) +- [x] 5.2 localStorage 저장/불러오기 +- [x] 5.3 대시보드에 설정 적용 (조건부 렌더링) + +### Phase 6: 마무리 +- [x] 6.1 스타일 정리 및 반응형 대응 +- [ ] 6.2 테스트 및 검증 (빌드 확인 필요) + +--- + +## 파일 구조 + +``` +src/components/business/CEODashboard/ +├── CEODashboard.tsx (수정 완료) +├── components.tsx +├── types.ts (수정 완료 - 설정 타입 추가) +├── dialogs/ +│ └── DashboardSettingsDialog.tsx (신규 생성 완료) +├── hooks/ +│ └── useDashboardSettings.ts (필요 시 추가) +└── sections/ + └── ... (기존) +``` + +--- + +## 참고사항 +- 기획서 Description 영역의 번호(01, 02 등)는 설명용이므로 UI에 구현하지 않음 +- 디자인은 프로젝트 기존 Dialog/Switch 컴포넌트 패턴 따름 + +## 구현 완료 (2026-01-08) +- DashboardSettingsDialog 컴포넌트 생성 +- 커스텀 ToggleSwitch 컴포넌트 (ON/OFF 라벨, 색상 지원) +- Collapsible 기반 아코디언 섹션 구현 +- localStorage 기반 설정 영속화 +- 대시보드 섹션 조건부 렌더링 적용 \ No newline at end of file diff --git a/claudedocs/[NEXT-2026-01-08] ceo-dashboard-session-context.md b/claudedocs/[NEXT-2026-01-08] ceo-dashboard-session-context.md new file mode 100644 index 00000000..0fe42a92 --- /dev/null +++ b/claudedocs/[NEXT-2026-01-08] ceo-dashboard-session-context.md @@ -0,0 +1,125 @@ +# CEO Dashboard 세션 컨텍스트 (2026-01-08) + +## 세션 요약 + +### 완료된 작업 +- [x] 세금 신고 카드: "3건" → "부가세 신고 D-15" (건수 제거) +- [x] 오늘의 이슈 카드: StatCards 스타일로 변경 +- [x] 문자열 count 스타일: `text-xl md:text-2xl font-medium` (작고 덜 굵게) +- [x] 새로고침 버튼 제거 +- [x] 항목 설정 버튼 → 페이지 헤더 오른쪽으로 이동 + +### 수정된 파일 +- `src/components/business/CEODashboard/CEODashboard.tsx` - 데이터, 버튼 위치 +- `src/components/business/CEODashboard/components.tsx` - IssueCardItem StatCards 스타일 +- `src/components/business/CEODashboard/sections/TodayIssueSection.tsx` - 항목 설정 버튼 제거 +- `src/components/business/CEODashboard/types.ts` - icon prop 추가 + +--- + +## 다음 세션 TODO + +### 1. 기획서 vs 구현 비교 점검 +- [ ] 기획서 스크린샷과 현재 구현 1:1 비교 +- [ ] 누락된 요소 확인 +- [ ] 임의 추가된 요소 제거 +- [ ] 빌드 확인 + +### 2. 기획서 기반 구현 정확도 개선 (우선순위 높음) + +#### 방안 A: RULES.md 강화 +**위치**: `~/.claude/RULES.md` - "Scope Discipline & Visual Reference Fidelity" 섹션 + +**추가할 규칙**: +```markdown +### 기획서/스크린샷 기반 구현 프로세스 +**Priority**: 🔴 **Triggers**: 기획서, 스크린샷, PDF 제공 시 + +**필수 단계**: +1. **요소 추출**: 스크린샷에서 모든 UI 요소 목록화 + - 버튼, 텍스트, 카드, 아이콘 등 식별 + - 위치, 스타일, 동작 기록 +2. **사용자 확인**: "이 요소들 맞아?" 확인 요청 +3. **기존 패턴 검색**: 프로젝트 내 유사 컴포넌트 찾기 +4. **구현**: 기획서 요소만 구현 (임의 추가 금지) +5. **검증 체크리스트**: 구현 후 기획서 vs 결과 비교표 제시 + +**금지 사항**: +- ❌ 기획서에 없는 버튼/기능 임의 추가 (예: 새로고침 버튼) +- ❌ 기획서와 다른 위치에 요소 배치 +- ❌ "있으면 좋겠다" 기반 추가 기능 +``` + +#### 방안 B: 스킬 생성 (`/sc:implement-ui`) +**위치**: `~/.claude/commands/sc_implement-ui.md` + +**스킬 플로우**: +``` +/sc:implement-ui @screenshot.png + +1. [분석] 스크린샷에서 UI 요소 추출 + - 버튼: [목록] + - 카드: [목록] + - 텍스트: [목록] + - 레이아웃: [설명] + +2. [확인] 사용자에게 요소 목록 확인 요청 + "이 요소들이 맞나요? 누락/추가할 것 있나요?" + +3. [패턴 검색] 기존 프로젝트에서 유사 컴포넌트 찾기 + - 검색 결과 제시 + - 재사용할 패턴 선택 + +4. [구현] 기획서 요소만 구현 + - 임의 추가 금지 + - 기존 패턴 따르기 + +5. [검증] 기획서 vs 구현 비교 체크리스트 + | 기획서 요소 | 구현 여부 | 위치 일치 | 스타일 일치 | + |------------|----------|----------|------------| + | 항목 설정 버튼 | ✅ | ✅ | ✅ | + | 새로고침 버튼 | ❌ (없음) | - | - | +``` + +**스킬 파일 예시**: +```markdown +# /sc:implement-ui - 기획서 기반 UI 구현 + +## 목적 +스크린샷/기획서를 정확하게 구현하기 위한 체계적 워크플로우 + +## 사용법 +/sc:implement-ui @screenshot.png +/sc:implement-ui @design.pdf "특정 섹션 설명" + +## 프로세스 +[위 플로우 내용] + +## 검증 규칙 +- 기획서에 있는 것만 구현 +- 없는 것은 절대 추가하지 않음 +- 구현 후 반드시 비교 체크리스트 제시 +``` + +--- + +## 문제점 분석 (이번 세션에서 발생한 이슈) + +### 발생한 문제 +1. **새로고침 버튼**: 기획서에 없는데 임의 추가 +2. **항목 설정 버튼 위치**: 기획서와 다른 위치에 배치 +3. **세금 신고 카드**: 기획서에 건수 없는데 "3건" 추가 + +### 원인 +- 기획서 꼼꼼히 확인 안 함 +- "있으면 좋겠다" 기반 임의 추가 +- 구현 전 요소 목록화 단계 누락 + +### 해결책 +- RULES.md 강화 + 스킬 생성으로 프로세스 강제 + +--- + +## 참고 파일 +- 기획서: `/Users/byeongcheolryu/Desktop/스크린샷 2026-01-07 오후 6.55.10.png` +- 체크리스트: `claudedocs/[IMPL-2026-01-07] ceo-dashboard-checklist.md` diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 6e5ec2ef..592039fe 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -1,6 +1,6 @@ # claudedocs 문서 맵 -> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-01-02) +> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-01-07) ## ⭐ 빠른 참조 @@ -131,6 +131,7 @@ claudedocs/ | 파일 | 설명 | |------|------| +| `[IMPL-2026-01-07] ceo-dashboard-checklist.md` | 🔴 **NEW** - 대표님 전용 대시보드 구현 체크리스트 (11개 섹션, 달력 포함) | | `dashboard-integration-complete.md` | 대시보드 통합 완료 | | `dashboard-cleanup-summary.md` | 정리 요약 | | `dashboard-migration-summary.md` | 마이그레이션 요약 | @@ -154,7 +155,8 @@ claudedocs/ | 파일 | 설명 | |------|------| -| `[DESIGN-2026-01-02] document-modal-common-component.md` | 🔴 **NEW** - 문서 모달 공통 컴포넌트 설계 요구사항 (6개 모달 분석, 헤더/결재라인/테이블 조합형) | +| `[REF-2026-01-07] nextjs-security-update-and-migration-plan.md` | 🔴 **NEW** - Next.js 보안 업데이트 (15.5.9) 및 16 마이그레이션 계획 | +| `[DESIGN-2026-01-02] document-modal-common-component.md` | 문서 모달 공통 컴포넌트 설계 요구사항 (6개 모달 분석, 헤더/결재라인/테이블 조합형) | | `[GUIDE] print-area-utility.md` | 인쇄 모달 printArea 유틸리티 가이드 (8개 모달 적용, print-utils.ts) | | `[GUIDE-2025-12-29] vercel-deployment.md` | Vercel 배포 가이드 (환경변수, CORS, 테스트 체크리스트) | | `[PLAN-2025-12-23] common-component-extraction-plan.md` | 공통 컴포넌트 추출 계획서 (Phase 1-4, 체크리스트 포함, ~1,900줄 절감) | diff --git a/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md b/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md index 392de785..82ded6b0 100644 --- a/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md +++ b/claudedocs/auth/[IMPL-2025-12-30] token-refresh-caching.md @@ -60,6 +60,10 @@ src/lib/api/ src/app/api/proxy/ └── [...path]/route.ts # Proxy (import from refresh-token) + +src/app/api/auth/ +├── check/route.ts # 🔧 인증 확인 API (2026-01-08 통합) +└── refresh/route.ts # 🔧 토큰 갱신 API (2026-01-08 통합) ``` ### 3.2 공통 모듈: `refresh-token.ts` @@ -302,12 +306,14 @@ Safari에서 `SameSite=Strict` + `Secure` 조합이 localhost에서 쿠키 저 ## 8. 관련 파일 -| 파일 | 역할 | -|------|------| -| `src/lib/api/refresh-token.ts` | 공통 토큰 갱신 모듈 (캐싱 로직) | -| `src/lib/api/fetch-wrapper.ts` | Server Actions용 fetch wrapper | -| `src/app/api/proxy/[...path]/route.ts` | 클라이언트 API 프록시 | -| `src/app/api/auth/login/route.ts` | 로그인 및 초기 토큰 설정 | +| 파일 | 역할 | 통합일 | +|------|------|--------| +| `src/lib/api/refresh-token.ts` | 공통 토큰 갱신 모듈 (캐싱 로직) | 2025-12-30 | +| `src/lib/api/fetch-wrapper.ts` | Server Actions용 fetch wrapper | 2025-12-30 | +| `src/app/api/proxy/[...path]/route.ts` | 클라이언트 API 프록시 | 2025-12-30 | +| `src/app/api/auth/login/route.ts` | 로그인 및 초기 토큰 설정 | - | +| `src/app/api/auth/check/route.ts` | 인증 상태 확인 API | 2026-01-08 | +| `src/app/api/auth/refresh/route.ts` | 토큰 갱신 프록시 API | 2026-01-08 | --- @@ -326,4 +332,84 @@ Safari에서 `SameSite=Strict` + `Secure` 조합이 localhost에서 쿠키 저 ### 9.3 향후 위험성 없음 - 5초 TTL은 충분히 짧아 토큰 갱신 지연 문제 없음 - 실패 시 다음 요청에서 새로 갱신 시도 -- 캐시는 메모리 기반이라 서버 재시작 시 자동 초기화 \ No newline at end of file +- 캐시는 메모리 기반이라 서버 재시작 시 자동 초기화 + +--- + +## 10. 업데이트 이력 + +### 10.1 [2026-01-08] 누락된 API 라우트 통합 + +**문제 발견:** +`/api/auth/check`와 `/api/auth/refresh` 라우트가 공유 캐시를 사용하지 않고 자체 fetch 로직을 사용하고 있었음. + +**증상:** +``` +🔍 Refresh API response status: 401 +❌ Refresh API failed: 401 {"error":"리프레시 토큰이 유효하지 않거나 만료되었습니다","error_code":"TOKEN_EXPIRED"} +⚠️ Returning 401 due to refresh failure +GET /api/auth/check 401 +``` + +**원인:** +1. `serverFetch`에서 refresh 성공 → Token Rotation으로 이전 refresh_token 폐기 +2. `/api/auth/check`가 동시에 호출됨 +3. 자체 fetch 로직으로 이미 폐기된 토큰 사용 시도 → 실패 → 로그인 페이지 이동 + +**해결:** +두 파일 모두 `refreshAccessToken()` 공유 함수를 사용하도록 수정: + +```typescript +// src/app/api/auth/check/route.ts +import { refreshAccessToken } from '@/lib/api/refresh-token'; + +const refreshResult = await refreshAccessToken(refreshToken, 'auth/check'); +``` + +```typescript +// src/app/api/auth/refresh/route.ts +import { refreshAccessToken } from '@/lib/api/refresh-token'; + +const refreshResult = await refreshAccessToken(refreshToken, 'api/auth/refresh'); +``` + +**결과:** +모든 refresh 경로가 동일한 5초 캐시를 공유하여 Token Rotation 충돌 방지. + +### 10.2 [2026-01-08] 53개 Server Actions 파일 수정 + +**문제:** +`redirect('/login')` 호출 시 발생하는 `NEXT_REDIRECT` 에러가 catch 블록에서 잡혀 `{ success: false }` 반환 → 무한 루프 + +**해결:** +모든 actions.ts 파일에 `isRedirectError` 처리 추가: + +```typescript +import { isRedirectError } from 'next/dist/client/components/redirect'; + +} catch (error) { + if (isRedirectError(error)) throw error; + // ... 기존 에러 처리 +} +``` + +### 10.3 [2026-01-08] refresh 실패 결과 캐시 버그 수정 + +**문제:** +refresh 실패 결과도 5초간 캐시되어, 후속 요청들이 모두 실패 결과를 받음. + +**해결:** +`refresh-token.ts`에서 성공한 결과만 캐시하도록 수정: + +```typescript +// 1. 캐시된 성공 결과가 유효하면 즉시 반환 +if (refreshCache.result && refreshCache.result.success && now - refreshCache.timestamp < REFRESH_CACHE_TTL) { + return refreshCache.result; +} + +// 2-1. 이전 refresh가 실패했으면 캐시 초기화 +if (refreshCache.result && !refreshCache.result.success) { + refreshCache.promise = null; + refreshCache.result = null; +} +``` \ No newline at end of file diff --git a/claudedocs/guides/[REF-2026-01-07] nextjs-security-update-and-migration-plan.md b/claudedocs/guides/[REF-2026-01-07] nextjs-security-update-and-migration-plan.md new file mode 100644 index 00000000..55d6724b --- /dev/null +++ b/claudedocs/guides/[REF-2026-01-07] nextjs-security-update-and-migration-plan.md @@ -0,0 +1,127 @@ +# Next.js 보안 업데이트 및 마이그레이션 계획 + +## 현재 상태 (2026-01-07) + +### 적용된 버전 +| 패키지 | 이전 버전 | 현재 버전 | 상태 | +|--------|-----------|-----------|------| +| next | 15.5.7 | **15.5.9** | ✅ 보안 패치 완료 | +| react | 19.2.1 | **19.2.3** | ✅ 보안 패치 완료 | +| react-dom | 19.2.1 | **19.2.3** | ✅ 보안 패치 완료 | + +### 해결된 취약점 +| CVE | 심각도 | 내용 | 상태 | +|-----|--------|------|------| +| CVE-2025-55184 | HIGH (7.5) | DoS - 무한 루프로 서버 중단 | ✅ 해결 | +| CVE-2025-55183 | MEDIUM (5.3) | Server Functions 소스코드 노출 | ✅ 해결 | +| CVE-2025-67779 | HIGH | CVE-2025-55184 완전 수정 | ✅ 해결 | + +### 남은 취약점 +| 패키지 | 심각도 | 내용 | 우선순위 | +|--------|--------|------|----------| +| js-yaml | MODERATE | Prototype Pollution (간접 의존성) | 낮음 | + +--- + +## Next.js 16 마이그레이션 계획 + +### 예상 작업량 +- **예상 소요 시간**: 4-8시간 +- **영향 파일 수**: 약 40개 + +### Breaking Changes 영향 분석 + +#### 1. middleware.ts → proxy.ts 변경 (중간 난이도) +``` +영향 파일: src/middleware.ts (316줄) +작업 내용: +- 파일명 변경: middleware.ts → proxy.ts +- 함수명 변경: export function middleware → export function proxy +- next-intl 호환: 이미 지원됨 +``` + +#### 2. Async Request APIs (가장 큰 작업) +``` +영향 파일: 36개 +수정 필요 위치: 52곳 + +변경 전 (현재 패턴): +const eventId = params.id as string; +const mode = searchParams.get('mode'); + +변경 후 (Next.js 16 패턴): +const { id } = await params; +const searchParamsResolved = await searchParams; +const mode = searchParamsResolved.get('mode'); +``` + +**영향받는 주요 영역**: +| 영역 | 파일 수 | +|------|---------| +| /accounting/* | 10개 | +| /hr/* | 6개 | +| /sales/* | 8개 | +| /boards/* | 6개 | +| 기타 | 6개 | + +#### 3. Turbopack 기본값 (영향 없음) +- `next.config.ts`에 이미 `turbopack: {}` 설정 있음 +- 커스텀 Webpack 설정 없음 → 호환 OK + +#### 4. cookies() 호출 (이미 호환) +- `src/lib/api/fetch-wrapper.ts`에서 `await cookies()` 사용 중 +- 추가 수정 불필요 + +### 마이그레이션 절차 + +```bash +# 1. feature 브랜치 생성 +git checkout -b feature/nextjs-16-migration + +# 2. 자동 마이그레이션 도구 실행 +npx @next/codemod@canary upgrade latest + +# 3. 수동 확인 및 수정 +# - middleware.ts → proxy.ts 변경 +# - params/searchParams async 변환 확인 + +# 4. 빌드 테스트 +npm run build + +# 5. 로컬 테스트 +npm run dev + +# 6. PR 생성 및 리뷰 +``` + +### 마이그레이션 체크리스트 +- [ ] feature 브랜치 생성 +- [ ] codemod 실행 +- [ ] middleware.ts → proxy.ts 변경 +- [ ] 함수명 middleware → proxy 변경 +- [ ] params/searchParams async 변환 (36개 파일) +- [ ] 빌드 테스트 통과 +- [ ] 주요 페이지 동작 테스트 +- [ ] PR 생성 및 머지 + +--- + +## 참고 자료 + +### 공식 문서 +- [Next.js 16 Release Blog](https://nextjs.org/blog/next-16) +- [Version 16 Upgrade Guide](https://nextjs.org/docs/app/guides/upgrading/version-16) +- [next-intl Middleware/Proxy Docs](https://next-intl.dev/docs/routing/middleware) + +### 보안 권고 +- [Next.js Security Update Dec 11, 2025](https://nextjs.org/blog/security-update-2025-12-11) +- [React DoS and Source Code Exposure](https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components) + +--- + +## 변경 이력 + +| 날짜 | 작업 | 담당 | +|------|------|------| +| 2026-01-07 | 보안 패치 적용 (15.5.9, 19.2.3) | Claude | +| - | Next.js 16 마이그레이션 | 예정 | diff --git a/src/app/[locale]/(protected)/hr/attendance/page.tsx b/src/app/[locale]/(protected)/hr/attendance/page.tsx index 0c54b435..e5670574 100644 --- a/src/app/[locale]/(protected)/hr/attendance/page.tsx +++ b/src/app/[locale]/(protected)/hr/attendance/page.tsx @@ -1,28 +1,21 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { MapPin } from 'lucide-react'; +import { MapPin, X, Clock, User } from 'lucide-react'; import { Button } from '@/components/ui/button'; import GoogleMap from '@/components/attendance/GoogleMap'; import AttendanceComplete from '@/components/attendance/AttendanceComplete'; import { checkIn, checkOut, getTodayAttendance } from '@/components/attendance/actions'; +import { useRouter } from 'next/navigation'; // ======================================== // 하드코딩 설정값 (MVP - 추후 API로 대체) // ======================================== -// TODO: 이 값들은 출퇴근관리 설정 페이지에서 관리됨 -// 설정 페이지 경로: /settings/attendance-settings -// API 연동 시: GET /api/settings/attendance 에서 조회 -// ──────────────────────────────────────── -// - radius: 출퇴근관리 설정의 allowedRadius 값 사용 -// - gpsDepartments: 로그인 사용자의 부서가 포함되어 있는지 체크 -// - gpsEnabled: false면 GPS 출퇴근 기능 비활성화 -// ──────────────────────────────────────── const SITE_LOCATION = { name: '본사', lat: 37.557358, lng: 126.864414, - radius: 100, // meters → 출퇴근관리 설정의 allowedRadius 값으로 대체 예정 + radius: 100, }; const TEST_USER = { @@ -31,15 +24,12 @@ const TEST_USER = { position: '직급명', }; -// 출퇴근 상태 타입 type AttendanceStatus = 'not-checked-in' | 'checked-in' | 'checked-out'; type ViewMode = 'main' | 'check-in-complete' | 'check-out-complete'; -export default function MobileAttendancePage() { - // Hydration 에러 방지: 클라이언트 마운트 상태 +export default function AttendancePage() { + const router = useRouter(); const [mounted, setMounted] = useState(false); - - // 상태 관리 const [currentTime, setCurrentTime] = useState('--:--:--'); const [currentDate, setCurrentDate] = useState(''); const [distance, setDistance] = useState(null); @@ -54,21 +44,27 @@ export default function MobileAttendancePage() { const [userPosition, setUserPosition] = useState(TEST_USER.position); const [isProcessing, setIsProcessing] = useState(false); const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(null); + const [isMobile, setIsMobile] = useState(false); - // 클라이언트 마운트 확인 useEffect(() => { setMounted(true); }, []); - // 오늘의 근태 상태 조회 (userId가 설정된 후에 조회) + useEffect(() => { + const checkScreenSize = () => { + setIsMobile(window.innerWidth < 768); + }; + checkScreenSize(); + window.addEventListener('resize', checkScreenSize); + return () => window.removeEventListener('resize', checkScreenSize); + }, []); + useEffect(() => { if (!mounted || userId === null) return; - const fetchTodayAttendance = async () => { try { const result = await getTodayAttendance(); if (result.success && result.data) { - // 이미 출근한 경우 if (result.data.checkIn) { setCheckInTime(result.data.checkIn); setAttendanceStatus(result.data.checkOut ? 'checked-out' : 'checked-in'); @@ -78,25 +74,20 @@ export default function MobileAttendancePage() { } } } catch (error) { - console.error('[MobileAttendancePage] fetchTodayAttendance error:', error); + console.error('[AttendancePage] fetchTodayAttendance error:', error); } }; - fetchTodayAttendance(); }, [mounted, userId]); - // 현재 시간 업데이트 (마운트 후에만 실행) useEffect(() => { if (!mounted) return; - const updateTime = () => { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); setCurrentTime(`${hours}:${minutes}:${seconds}`); - - // 날짜 포맷 const year = now.getFullYear(); const month = now.getMonth() + 1; const date = now.getDate(); @@ -104,16 +95,13 @@ export default function MobileAttendancePage() { const day = dayNames[now.getDay()]; setCurrentDate(`${year}년 ${month}월 ${date}일 (${day})`); }; - updateTime(); const interval = setInterval(updateTime, 1000); return () => clearInterval(interval); }, [mounted]); - // localStorage에서 사용자 정보 가져오기 (마운트 후에만 실행) useEffect(() => { if (!mounted) return; - const userDataStr = localStorage.getItem('user'); if (userDataStr) { try { @@ -128,171 +116,171 @@ export default function MobileAttendancePage() { } }, [mounted]); - // 거리 변경 콜백 const handleDistanceChange = useCallback((dist: number, inRange: boolean, location?: { lat: number; lng: number }) => { setDistance(dist); setIsInRange(inRange); - if (location) { - setUserLocation(location); - } + if (location) setUserLocation(location); }, []); - // 출근하기 const handleCheckIn = async () => { if (!isInRange || isProcessing) return; - setIsProcessing(true); - const now = new Date(); - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const seconds = String(now.getSeconds()).padStart(2, '0'); - const timeStr = `${hours}:${minutes}:${seconds}`; - + const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; try { const result = await checkIn({ checkIn: timeStr, - gpsData: userLocation - ? { - latitude: userLocation.lat, - longitude: userLocation.lng, - } - : undefined, + gpsData: userLocation ? { latitude: userLocation.lat, longitude: userLocation.lng } : undefined, }); - if (result.success) { setCheckInTime(timeStr); setAttendanceStatus('checked-in'); setViewMode('check-in-complete'); - } else { - console.error('[MobileAttendancePage] Check-in failed:', result.error); - // TODO: 에러 토스트 표시 } } catch (error) { - console.error('[MobileAttendancePage] Check-in error:', error); + console.error('[AttendancePage] Check-in error:', error); } finally { setIsProcessing(false); } }; - // 퇴근하기 const handleCheckOut = async () => { if (!isInRange || isProcessing) return; - setIsProcessing(true); - const now = new Date(); - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const seconds = String(now.getSeconds()).padStart(2, '0'); - const timeStr = `${hours}:${minutes}:${seconds}`; - + const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; try { const result = await checkOut({ checkOut: timeStr, - gpsData: userLocation - ? { - latitude: userLocation.lat, - longitude: userLocation.lng, - } - : undefined, + gpsData: userLocation ? { latitude: userLocation.lat, longitude: userLocation.lng } : undefined, }); - if (result.success) { setCheckOutTime(timeStr); setAttendanceStatus('checked-out'); setViewMode('check-out-complete'); - } else { - console.error('[MobileAttendancePage] Check-out failed:', result.error); - // TODO: 에러 토스트 표시 } } catch (error) { - console.error('[MobileAttendancePage] Check-out error:', error); + console.error('[AttendancePage] Check-out error:', error); } finally { setIsProcessing(false); } }; - // 완료 화면에서 확인 클릭 - const handleConfirm = () => { - setViewMode('main'); - }; + const handleConfirm = () => setViewMode('main'); + const handleClose = () => router.back(); - // 마운트 전 로딩 UI (Hydration 에러 방지) if (!mounted) { return ( -
-
-

출퇴근하기

-
-
+
+
-

로딩 중...

+

로딩 중...

); } - // 완료 화면 렌더링 if (viewMode === 'check-in-complete') { return ( -
- +
+
); } if (viewMode === 'check-out-complete') { return ( -
- +
+
); } - // 버튼 활성화 상태 - // - 출근: 아직 출근 안 했거나, 퇴근한 경우 (다시 출근 가능) - // - 퇴근: 출근한 경우에만 가능 const canCheckIn = isInRange && attendanceStatus !== 'checked-in' && !isProcessing; const canCheckOut = isInRange && attendanceStatus === 'checked-in' && !isProcessing; - return ( -
- {/* 타이틀 */} -
-

출퇴근하기

+ // 모바일 레이아웃 + if (isMobile) { + return ( +
+
+ + {distance !== null && ( +
+
+ + + {distance < 1000 ? `${Math.round(distance)}m` : `${(distance / 1000).toFixed(1)}km`} + {isInRange && ' (범위 내)'} + +
+
+ )} +
+ +
+
+
+

출퇴근하기

+

현재 위치에서 출퇴근을 기록하세요

+
+ +
+ +
+
+ +
+
+

{userName}

+

{userDepartment} {userPosition}

+
+
+ +
+
+ + 현재 시간 +
+

{currentTime}

+
+ + + + {!isInRange && distance !== null && ( +

출퇴근 가능 범위({SITE_LOCATION.radius}m) 밖에 있습니다.

+ )} +
+ ); + } - {/* 지도 영역 */} + // PC 레이아웃 - 좌측 지도 + 우측 패널 + return ( +
- - - {/* 거리 표시 오버레이 */} + {distance !== null && ( -
-
+
+
- {distance < 1000 - ? `${Math.round(distance)}m` - : `${(distance / 1000).toFixed(1)}km`} + {distance < 1000 ? `${Math.round(distance)}m` : `${(distance / 1000).toFixed(1)}km`} {isInRange && ' (범위 내)'}
@@ -300,62 +288,60 @@ export default function MobileAttendancePage() { )}
- {/* 사용자 정보 + 시간 + 버튼 */} -
- {/* 사용자 정보 */} -
-
- {userName.charAt(0)} -
+
+
-

{userName}

-

- {userDepartment} {userPosition} -

+

출퇴근하기

+

현재 위치에서 출퇴근을 기록하세요

+
- {/* 현재 시간 */} -
-

{currentTime}

-

{currentDate}

- {attendanceStatus === 'checked-in' && ( -

출근중

- )} -
+
+
+
+ +
+
+

{userName}

+

{userDepartment} · {userPosition}

+
+
+ +
+
+ + 현재 시간 +
+

{currentTime}

+
- {/* 출근/퇴근 버튼 */} -
- -
- {/* 범위 밖 경고 */} - {!isInRange && distance !== null && ( -

- 출퇴근 가능 범위({SITE_LOCATION.radius}m) 밖에 있습니다. -

- )} + {!isInRange && distance !== null && ( +

출퇴근 가능 범위({SITE_LOCATION.radius}m) 밖에 있습니다.

+ )} + + {attendanceStatus === 'checked-in' && ( +
+

출근 완료

+

출근 시간: {checkInTime}

+
+ )} +
); diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx index cdea893d..c6581dd5 100644 --- a/src/app/[locale]/(protected)/quality/qms/page.tsx +++ b/src/app/[locale]/(protected)/quality/qms/page.tsx @@ -98,7 +98,7 @@ export default function QualityInspectionPage() { }; return ( -
+
-
+
{/* Left Panel: Report List */} -
+
{/* Middle Panel: Route List */} -
+
{/* Right Panel: Documents */} -
+
return transformApiToFrontend(result.data); } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] getBadDebtById error:', error); return null; } @@ -352,6 +356,7 @@ export async function getBadDebtSummary(): Promise return result.data; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] getBadDebtSummary error:', error); return null; } @@ -396,6 +401,7 @@ export async function createBadDebt( data: transformApiToFrontend(result.data), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] createBadDebt error:', error); return { success: false, @@ -444,6 +450,7 @@ export async function updateBadDebt( data: transformApiToFrontend(result.data), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] updateBadDebt error:', error); return { success: false, @@ -479,6 +486,7 @@ export async function deleteBadDebt(id: string): Promise<{ success: boolean; err return { success: true }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] deleteBadDebt error:', error); return { success: false, @@ -517,6 +525,7 @@ export async function toggleBadDebt(id: string): Promise<{ success: boolean; dat data: transformApiToFrontend(result.data), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] toggleBadDebt error:', error); return { success: false, @@ -565,6 +574,7 @@ export async function addBadDebtMemo( }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] addBadDebtMemo error:', error); return { success: false, @@ -601,6 +611,7 @@ export async function deleteBadDebtMemo( return { success: true }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BadDebtActions] deleteBadDebtMemo error:', error); return { success: false, diff --git a/src/components/accounting/BankTransactionInquiry/actions.ts b/src/components/accounting/BankTransactionInquiry/actions.ts index a805f1b8..04a02af1 100644 --- a/src/components/accounting/BankTransactionInquiry/actions.ts +++ b/src/components/accounting/BankTransactionInquiry/actions.ts @@ -1,5 +1,7 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { BankTransaction, TransactionKind } from './types'; @@ -155,6 +157,7 @@ export async function getBankTransactionList(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BankTransactionActions] getBankTransactionList error:', error); return { success: false, @@ -223,6 +226,7 @@ export async function getBankTransactionSummary(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BankTransactionActions] getBankTransactionSummary error:', error); return { success: false, @@ -270,6 +274,7 @@ export async function getBankAccountOptions(): Promise<{ data: result.data as BankAccountOption[], }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[BankTransactionActions] getBankAccountOptions error:', error); return { success: false, diff --git a/src/components/accounting/BillManagement/actions.ts b/src/components/accounting/BillManagement/actions.ts index 029b0c41..dbbcd0db 100644 --- a/src/components/accounting/BillManagement/actions.ts +++ b/src/components/accounting/BillManagement/actions.ts @@ -1,5 +1,7 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { BillRecord, BillApiData, BillStatus } from './types'; import { transformApiToFrontend, transformFrontendToApi } from './types'; @@ -103,6 +105,7 @@ export async function getBills(params: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[getBills] Error:', error); return { success: false, @@ -145,6 +148,7 @@ export async function getBill(id: string): Promise<{ data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[getBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -191,6 +195,7 @@ export async function createBill( data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[createBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -238,6 +243,7 @@ export async function updateBill( data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[updateBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -267,6 +273,7 @@ export async function deleteBill(id: string): Promise<{ success: boolean; error? return { success: true }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[deleteBill] Error:', error); return { success: false, error: 'Server error' }; } @@ -305,6 +312,7 @@ export async function updateBillStatus( data: transformApiToFrontend(result.data as BillApiData), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[updateBillStatus] Error:', error); return { success: false, error: 'Server error' }; } @@ -368,6 +376,7 @@ export async function getBillSummary(params: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[getBillSummary] Error:', error); return { success: false, error: 'Server error' }; } @@ -410,6 +419,7 @@ export async function getClients(): Promise<{ })), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[getClients] Error:', error); return { success: false, error: 'Server error' }; } diff --git a/src/components/accounting/CardTransactionInquiry/actions.ts b/src/components/accounting/CardTransactionInquiry/actions.ts index 2d50c6da..587c32b0 100644 --- a/src/components/accounting/CardTransactionInquiry/actions.ts +++ b/src/components/accounting/CardTransactionInquiry/actions.ts @@ -1,5 +1,7 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { CardTransaction } from './types'; @@ -158,6 +160,7 @@ export async function getCardTransactionList(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[CardTransactionActions] getCardTransactionList error:', error); return { success: false, @@ -226,6 +229,7 @@ export async function getCardTransactionSummary(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[CardTransactionActions] getCardTransactionSummary error:', error); return { success: false, @@ -277,6 +281,7 @@ export async function bulkUpdateAccountCode( updatedCount: result.data?.updated_count || 0, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[CardTransactionActions] bulkUpdateAccountCode error:', error); return { success: false, diff --git a/src/components/accounting/DailyReport/actions.ts b/src/components/accounting/DailyReport/actions.ts index 39b40bd5..492ec86b 100644 --- a/src/components/accounting/DailyReport/actions.ts +++ b/src/components/accounting/DailyReport/actions.ts @@ -1,5 +1,7 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { cookies } from 'next/headers'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { NoteReceivableItem, DailyAccountItem, MatchStatus } from './types'; @@ -116,6 +118,7 @@ export async function getNoteReceivables(params?: { data: items, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[DailyReportActions] getNoteReceivables error:', error); return { success: false, @@ -173,6 +176,7 @@ export async function getDailyAccounts(params?: { data: items, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[DailyReportActions] getDailyAccounts error:', error); return { success: false, @@ -254,6 +258,7 @@ export async function getDailyReportSummary(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[DailyReportActions] getDailyReportSummary error:', error); return { success: false, @@ -310,6 +315,7 @@ export async function exportDailyReportExcel(params?: { filename, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[DailyReportActions] exportDailyReportExcel error:', error); return { success: false, diff --git a/src/components/accounting/DepositManagement/actions.ts b/src/components/accounting/DepositManagement/actions.ts index 3609b57f..51278941 100644 --- a/src/components/accounting/DepositManagement/actions.ts +++ b/src/components/accounting/DepositManagement/actions.ts @@ -1,5 +1,7 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { DepositRecord, DepositType, DepositStatus } from './types'; diff --git a/src/components/accounting/ExpectedExpenseManagement/actions.ts b/src/components/accounting/ExpectedExpenseManagement/actions.ts index 568344f5..c900fd08 100644 --- a/src/components/accounting/ExpectedExpenseManagement/actions.ts +++ b/src/components/accounting/ExpectedExpenseManagement/actions.ts @@ -1,5 +1,7 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus } from './types'; @@ -188,6 +190,7 @@ export async function getExpectedExpenses(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getExpectedExpenses error:', error); return { success: false, @@ -236,6 +239,7 @@ export async function getExpectedExpenseById(id: string): Promise<{ data: transformApiToFrontend(result.data), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getExpectedExpenseById error:', error); return { success: false, @@ -277,6 +281,7 @@ export async function createExpectedExpense( data: transformApiToFrontend(result.data), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] createExpectedExpense error:', error); return { success: false, @@ -319,6 +324,7 @@ export async function updateExpectedExpense( data: transformApiToFrontend(result.data), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] updateExpectedExpense error:', error); return { success: false, @@ -350,6 +356,7 @@ export async function deleteExpectedExpense(id: string): Promise<{ success: bool return { success: true }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] deleteExpectedExpense error:', error); return { success: false, @@ -393,6 +400,7 @@ export async function deleteExpectedExpenses(ids: string[]): Promise<{ deletedCount: result.data?.deleted_count, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] deleteExpectedExpenses error:', error); return { success: false, @@ -440,6 +448,7 @@ export async function updateExpectedPaymentDate( updatedCount: result.data?.updated_count, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] updateExpectedPaymentDate error:', error); return { success: false, @@ -498,6 +507,7 @@ export async function getExpectedExpenseSummary(params?: { data: result.data, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getExpectedExpenseSummary error:', error); return { success: false, @@ -542,6 +552,7 @@ export async function getClients(): Promise<{ })), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getClients error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } @@ -585,6 +596,7 @@ export async function getBankAccounts(): Promise<{ })), }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ExpectedExpenseActions] getBankAccounts error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } diff --git a/src/components/accounting/PurchaseManagement/actions.ts b/src/components/accounting/PurchaseManagement/actions.ts index a7854c03..f78c903b 100644 --- a/src/components/accounting/PurchaseManagement/actions.ts +++ b/src/components/accounting/PurchaseManagement/actions.ts @@ -12,6 +12,8 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { PurchaseRecord, PurchaseType } from './types'; diff --git a/src/components/accounting/ReceivablesStatus/actions.ts b/src/components/accounting/ReceivablesStatus/actions.ts index cf4f2b1a..3c5507b9 100644 --- a/src/components/accounting/ReceivablesStatus/actions.ts +++ b/src/components/accounting/ReceivablesStatus/actions.ts @@ -1,5 +1,7 @@ 'use server'; + +import { isRedirectError } from 'next/dist/client/components/redirect'; import { cookies } from 'next/headers'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { VendorReceivables, CategoryType, MonthlyAmount, ReceivablesListResponse, MemoUpdateRequest } from './types'; @@ -55,7 +57,6 @@ function transformItem(item: VendorReceivablesApi): VendorReceivables { total: cat.amounts.total, }, })), - memo: (item as VendorReceivablesApi & { memo?: string }).memo ?? '', }; } @@ -127,6 +128,7 @@ export async function getReceivablesList(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ReceivablesActions] getReceivablesList error:', error); return { success: false, @@ -207,6 +209,7 @@ export async function getReceivablesSummary(params?: { }, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ReceivablesActions] getReceivablesSummary error:', error); return { success: false, @@ -262,6 +265,7 @@ export async function updateOverdueStatus( updatedCount: result.data?.updated_count || updates.length, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ReceivablesActions] updateOverdueStatus error:', error); return { success: false, @@ -317,6 +321,7 @@ export async function updateMemos( updatedCount: result.data?.updated_count || memos.length, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ReceivablesActions] updateMemos error:', error); return { success: false, @@ -383,6 +388,7 @@ export async function exportReceivablesExcel(params?: { filename, }; } catch (error) { + if (isRedirectError(error)) throw error; console.error('[ReceivablesActions] exportReceivablesExcel error:', error); return { success: false, diff --git a/src/components/accounting/ReceivablesStatus/index.tsx b/src/components/accounting/ReceivablesStatus/index.tsx index 34d6230b..8d827269 100644 --- a/src/components/accounting/ReceivablesStatus/index.tsx +++ b/src/components/accounting/ReceivablesStatus/index.tsx @@ -1,10 +1,11 @@ 'use client'; import { useState, useMemo, useCallback, useEffect, useRef, useTransition } from 'react'; -import { Download, FileText, Save, Loader2, RefreshCw } from 'lucide-react'; +import { Download, FileText, Save, Loader2, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableFooter } from '@/components/ui/table'; import { @@ -537,7 +538,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma {categoryData.amounts.values.map((amount, monthIndex) => ( {formatAmount(amount)} @@ -552,20 +553,39 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma }); // 메모 행 추가 (마지막 행) + const isMemoExpanded = expandedMemos.has(vendor.id); rows.push( - {/* 구분: 메모 */} + {/* 구분: 메모 + 접기/펼치기 버튼 */} - 메모 +
+ 메모 + +
{/* 메모 입력 - 모든 월 컬럼 + 합계 컬럼 병합 */} - - +