From 736c29a0076533d83c351e7b5394b90edeb26023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Fri, 16 Jan 2026 18:34:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20CEO=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=BA=98=EB=A6=B0=EB=8D=94=EC=97=90=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20=EC=98=A4=EB=8A=98?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EC=8A=88=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 캘린더에 오늘의 이슈 데이터 표시 기능 추가 - 이슈 클릭 시 상세 페이지 이동 기능 구현 - 캘린더 필터에 '이슈' 옵션 추가 - TodayIssueListItem에 date 필드 추가 - 오늘의 이슈 섹션 반응형 그리드 레이아웃 개선 - 필터 드롭다운에 항목별 건수 표시 - 캘린더 상세 목록 높이 동적 조절 (calc(100vh-400px)) - 테스트 URL 페이지 기능 개선 Co-Authored-By: Claude Opus 4.5 --- .../ConstructionTestUrlsClient.tsx | 41 ++++- .../dev/construction-test-urls/actions.ts | 44 ++++- .../dev/test-urls/TestUrlsClient.tsx | 67 ++++++- .../(protected)/dev/test-urls/actions.ts | 87 ++++++--- .../business/CEODashboard/CEODashboard.tsx | 1 + .../business/CEODashboard/mockData.ts | 170 ++++++++++++++++++ .../CEODashboard/sections/CalendarSection.tsx | 134 ++++++++++++-- .../sections/TodayIssueSection.tsx | 109 ++++++----- src/components/business/CEODashboard/types.ts | 1 + 9 files changed, 546 insertions(+), 108 deletions(-) diff --git a/src/app/[locale]/(protected)/dev/construction-test-urls/ConstructionTestUrlsClient.tsx b/src/app/[locale]/(protected)/dev/construction-test-urls/ConstructionTestUrlsClient.tsx index db86d366..5828b6e7 100644 --- a/src/app/[locale]/(protected)/dev/construction-test-urls/ConstructionTestUrlsClient.tsx +++ b/src/app/[locale]/(protected)/dev/construction-test-urls/ConstructionTestUrlsClient.tsx @@ -1,7 +1,33 @@ 'use client'; import { useState, useEffect } from 'react'; -import { ExternalLink, Copy, Check, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react'; +import { + ExternalLink, + Copy, + Check, + ChevronDown, + ChevronRight, + RefreshCw, + Link, + Home, + Monitor, + BarChart3, + FileText, + type LucideIcon, +} from 'lucide-react'; + +// Lucide 아이콘 매핑 (이모지 대신 사용 - Chrome DevTools MCP JSON 직렬화 오류 방지) +const iconComponents: Record = { + Home, + Monitor, + BarChart3, + FileText, +}; + +function CategoryIcon({ name, className }: { name: string; className?: string }) { + const IconComponent = iconComponents[name] || FileText; + return ; +} export interface UrlItem { name: string; @@ -97,7 +123,7 @@ function CategorySection({ category, baseUrl }: { category: UrlCategory; baseUrl ) : ( )} - {category.icon} +

{category.title}

@@ -196,8 +222,9 @@ export default function ConstructionTestUrlsClient({ initialData, lastUpdated }: {/* Header */}
-

- 🏭 주일기업 테스트 URL 목록 +

+ + 주일기업 테스트 URL 목록

@@ -255,7 +282,7 @@ export default function ConstructionTestUrlsClient({ initialData, lastUpdated }: {/* Footer */}

- 📁 데이터 소스: claudedocs/[REF] construction-pages-test-urls.md + 데이터 소스: claudedocs/[REF] construction-pages-test-urls.md

md 파일 수정 후 새로고침하면 자동 반영! diff --git a/src/app/[locale]/(protected)/dev/construction-test-urls/actions.ts b/src/app/[locale]/(protected)/dev/construction-test-urls/actions.ts index ae412ee4..e8c41dd7 100644 --- a/src/app/[locale]/(protected)/dev/construction-test-urls/actions.ts +++ b/src/app/[locale]/(protected)/dev/construction-test-urls/actions.ts @@ -4,18 +4,43 @@ import { promises as fs } from 'fs'; import path from 'path'; import type { UrlCategory, UrlItem } from './ConstructionTestUrlsClient'; -// 아이콘 매핑 +// 아이콘 매핑 (Lucide 아이콘 이름 사용 - 이모지는 Chrome DevTools MCP에서 JSON 직렬화 오류 발생) const iconMap: Record = { - '기본': '🏠', - '시스템': '💻', - '대시보드': '📊', + '기본': 'Home', + '시스템': 'Monitor', + '대시보드': 'BarChart3', }; function getIcon(title: string): string { for (const [key, icon] of Object.entries(iconMap)) { if (title.includes(key)) return icon; } - return '📄'; + return 'FileText'; +} + +// 이모지를 텍스트로 변환 (Chrome DevTools MCP JSON 직렬화 오류 방지) +function convertEmojiToText(text: string): string { + // 먼저 특정 이모지를 의미있는 텍스트로 변환 + let result = text + .replace(/✅/g, '[완료]') + .replace(/⚠️?/g, '[주의]') + .replace(/🧪/g, '[테스트]') + .replace(/🆕/g, '[NEW]') + .replace(/•/g, '-'); + + // 나머지 모든 이모지 및 특수 유니코드 문자 제거 + // Unicode emoji 범위와 variation selectors 제거 + result = result + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') // 이모지 범위 + .replace(/[\u{2600}-\u{26FF}]/gu, '') // 기타 기호 + .replace(/[\u{2700}-\u{27BF}]/gu, '') // 딩뱃 + .replace(/[\u{FE00}-\u{FE0F}]/gu, '') // Variation Selectors + .replace(/[\u{1F000}-\u{1F02F}]/gu, '') // 마작 타일 + .replace(/[\u{1F0A0}-\u{1F0FF}]/gu, '') // 플레잉 카드 + .replace(/[\u200D]/g, '') // Zero Width Joiner + .trim(); + + return result; } function parseTableRow(line: string): UrlItem | null { @@ -25,9 +50,10 @@ function parseTableRow(line: string): UrlItem | null { if (parts.length < 2) return null; if (parts[0] === '페이지' || parts[0].startsWith('---')) return null; - const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거 + const name = convertEmojiToText(parts[0].replace(/\*\*/g, '')); // **bold** 제거 + 이모지 변환 const url = parts[1].replace(/`/g, ''); // backtick 제거 - const status = parts[2] || undefined; + const rawStatus = parts[2] || ''; + const status = convertEmojiToText(rawStatus) || undefined; // URL이 /ko로 시작하는지 확인 if (!url.startsWith('/ko')) return null; @@ -63,7 +89,7 @@ function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: categories.push(currentCategory); } - const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim(); + const title = convertEmojiToText(line.replace('## ', '')); currentCategory = { title, icon: getIcon(title), @@ -81,7 +107,7 @@ function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: currentCategory.subCategories.push(currentSubCategory); } - const subTitle = line.replace('### ', '').trim(); + const subTitle = convertEmojiToText(line.replace('### ', '')); // "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로 if (subTitle === '메인 페이지') { currentSubCategory = null; diff --git a/src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx b/src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx index b2980d27..4366d288 100644 --- a/src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx +++ b/src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx @@ -1,7 +1,59 @@ 'use client'; import { useState, useEffect } from 'react'; -import { ExternalLink, Copy, Check, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react'; +import { + ExternalLink, + Copy, + Check, + ChevronDown, + ChevronRight, + RefreshCw, + Link, + Home, + Users, + DollarSign, + Package, + Factory, + Boxes, + FlaskConical, + PackageCheck, + Settings, + FileText, + Wallet, + ClipboardList, + BarChart3, + User, + Building2, + CreditCard, + Headphones, + type LucideIcon, +} from 'lucide-react'; + +// Lucide 아이콘 매핑 (이모지 대신 사용 - Chrome DevTools MCP JSON 직렬화 오류 방지) +const iconComponents: Record = { + Home, + Users, + DollarSign, + Package, + Factory, + Boxes, + FlaskConical, + PackageCheck, + Settings, + FileText, + Wallet, + ClipboardList, + BarChart3, + User, + Building2, + CreditCard, + Headphones, +}; + +function CategoryIcon({ name, className }: { name: string; className?: string }) { + const IconComponent = iconComponents[name] || FileText; + return ; +} export interface UrlItem { name: string; @@ -97,7 +149,7 @@ function CategorySection({ category, baseUrl }: { category: UrlCategory; baseUrl ) : ( )} - {category.icon} +

{category.title}

@@ -196,8 +248,9 @@ export default function TestUrlsClient({ initialData, lastUpdated }: TestUrlsCli {/* Header */}
-

- 🔗 테스트 URL 목록 +

+ + 테스트 URL 목록

@@ -255,7 +308,7 @@ export default function TestUrlsClient({ initialData, lastUpdated }: TestUrlsCli {/* Footer */}

- 📁 데이터 소스: claudedocs/[REF] all-pages-test-urls.md + 데이터 소스: claudedocs/[REF] all-pages-test-urls.md

md 파일 수정 후 새로고침하면 자동 반영! diff --git a/src/app/[locale]/(protected)/dev/test-urls/actions.ts b/src/app/[locale]/(protected)/dev/test-urls/actions.ts index 54f309c4..fc62259b 100644 --- a/src/app/[locale]/(protected)/dev/test-urls/actions.ts +++ b/src/app/[locale]/(protected)/dev/test-urls/actions.ts @@ -4,34 +4,70 @@ import { promises as fs } from 'fs'; import path from 'path'; import type { UrlCategory, UrlItem } from './TestUrlsClient'; -// 아이콘 매핑 +// 아이콘 매핑 (Lucide 아이콘 이름 사용 - 이모지는 Chrome DevTools MCP에서 JSON 직렬화 오류 발생) const iconMap: Record = { - '기본': '🏠', - '인사관리': '👥', - 'HR': '👥', - '판매관리': '💰', - 'Sales': '💰', - '기준정보관리': '📦', - 'Master Data': '📦', - '생산관리': '🏭', - 'Production': '🏭', - '설정': '⚙️', - 'Settings': '⚙️', - '전자결재': '📝', - 'Approval': '📝', - '회계관리': '💵', - 'Accounting': '💵', - '게시판': '📋', - 'Board': '📋', - '보고서': '📊', - 'Reports': '📊', + '기본': 'Home', + '인사관리': 'Users', + 'HR': 'Users', + '판매관리': 'DollarSign', + 'Sales': 'DollarSign', + '기준정보관리': 'Package', + 'Master Data': 'Package', + '생산관리': 'Factory', + 'Production': 'Factory', + '자재관리': 'Boxes', + 'Material': 'Boxes', + '품질관리': 'FlaskConical', + 'Quality': 'FlaskConical', + '출고관리': 'PackageCheck', + 'Outbound': 'PackageCheck', + '설정': 'Settings', + 'Settings': 'Settings', + '전자결재': 'FileText', + 'Approval': 'FileText', + '회계관리': 'Wallet', + 'Accounting': 'Wallet', + '게시판': 'ClipboardList', + 'Board': 'ClipboardList', + '보고서': 'BarChart3', + 'Reports': 'BarChart3', + '계정': 'User', + '회사': 'Building2', + '구독': 'CreditCard', + '고객센터': 'Headphones', + 'Customer': 'Headphones', }; function getIcon(title: string): string { for (const [key, icon] of Object.entries(iconMap)) { if (title.includes(key)) return icon; } - return '📄'; + return 'FileText'; +} + +// 이모지를 텍스트로 변환 (Chrome DevTools MCP JSON 직렬화 오류 방지) +function convertEmojiToText(text: string): string { + // 먼저 특정 이모지를 의미있는 텍스트로 변환 + let result = text + .replace(/✅/g, '[완료]') + .replace(/⚠️?/g, '[주의]') + .replace(/🧪/g, '[테스트]') + .replace(/🆕/g, '[NEW]') + .replace(/•/g, '-'); + + // 나머지 모든 이모지 및 특수 유니코드 문자 제거 + // Unicode emoji 범위와 variation selectors 제거 + result = result + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') // 이모지 범위 + .replace(/[\u{2600}-\u{26FF}]/gu, '') // 기타 기호 + .replace(/[\u{2700}-\u{27BF}]/gu, '') // 딩뱃 + .replace(/[\u{FE00}-\u{FE0F}]/gu, '') // Variation Selectors + .replace(/[\u{1F000}-\u{1F02F}]/gu, '') // 마작 타일 + .replace(/[\u{1F0A0}-\u{1F0FF}]/gu, '') // 플레잉 카드 + .replace(/[\u200D]/g, '') // Zero Width Joiner + .trim(); + + return result; } function parseTableRow(line: string): UrlItem | null { @@ -41,9 +77,10 @@ function parseTableRow(line: string): UrlItem | null { if (parts.length < 2) return null; if (parts[0] === '페이지' || parts[0].startsWith('---')) return null; - const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거 + const name = convertEmojiToText(parts[0].replace(/\*\*/g, '')); // **bold** 제거 + 이모지 변환 const url = parts[1].replace(/`/g, ''); // backtick 제거 - const status = parts[2] || undefined; + const rawStatus = parts[2] || ''; + const status = convertEmojiToText(rawStatus) || undefined; // URL이 /ko로 시작하는지 확인 if (!url.startsWith('/ko')) return null; @@ -79,7 +116,7 @@ function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: categories.push(currentCategory); } - const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim(); + const title = convertEmojiToText(line.replace('## ', '')); currentCategory = { title, icon: getIcon(title), @@ -97,7 +134,7 @@ function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: currentCategory.subCategories.push(currentSubCategory); } - const subTitle = line.replace('### ', '').trim(); + const subTitle = convertEmojiToText(line.replace('### ', '')); // "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로 if (subTitle === '메인 페이지') { currentSubCategory = null; diff --git a/src/components/business/CEODashboard/CEODashboard.tsx b/src/components/business/CEODashboard/CEODashboard.tsx index 30bd1996..3336f6d4 100644 --- a/src/components/business/CEODashboard/CEODashboard.tsx +++ b/src/components/business/CEODashboard/CEODashboard.tsx @@ -287,6 +287,7 @@ export function CEODashboard() { {dashboardSettings.calendar && ( diff --git a/src/components/business/CEODashboard/mockData.ts b/src/components/business/CEODashboard/mockData.ts index d91fb955..b25cb398 100644 --- a/src/components/business/CEODashboard/mockData.ts +++ b/src/components/business/CEODashboard/mockData.ts @@ -21,6 +21,7 @@ export const mockData: CEODashboardData = { badge: '수주 성공', content: 'A전자 신규 수주 450,000,000원 확정', time: '10분 전', + date: '2026-01-16', needsApproval: false, path: '/sales/order-management-sales', }, @@ -29,6 +30,7 @@ export const mockData: CEODashboardData = { badge: '주식 이슈', content: 'B물산 미수금 15,000,000원 연체 15일', time: '1시간 전', + date: '2026-01-16', needsApproval: false, path: '/accounting/receivables-status', }, @@ -37,6 +39,7 @@ export const mockData: CEODashboardData = { badge: '직정 제고', content: '원자재 3종 안전재고 미달', time: '20시간 전', + date: '2026-01-16', needsApproval: false, path: '/material/stock-status', }, @@ -45,6 +48,7 @@ export const mockData: CEODashboardData = { badge: '지출예상내역서', content: '품의서명 외 5건 (2,500,000원)', time: '20시간 전', + date: '2026-01-16', needsApproval: true, path: '/approval/inbox', }, @@ -53,6 +57,7 @@ export const mockData: CEODashboardData = { badge: '세금 신고', content: '4분기 부가세 신고 D-15', time: '20시간 전', + date: '2026-01-16', needsApproval: false, path: '/accounting/tax', }, @@ -61,6 +66,7 @@ export const mockData: CEODashboardData = { badge: '결재 요청', content: '법인카드 사용 내역 승인 요청 (김철수)', time: '30분 전', + date: '2026-01-16', needsApproval: true, path: '/approval/inbox', }, @@ -69,6 +75,7 @@ export const mockData: CEODashboardData = { badge: '수주 성공', content: 'C건설 추가 발주 120,000,000원 확정', time: '2시간 전', + date: '2026-01-16', needsApproval: false, path: '/sales/order-management-sales', }, @@ -77,6 +84,7 @@ export const mockData: CEODashboardData = { badge: '기타', content: '신규 거래처 D산업 등록 완료', time: '3시간 전', + date: '2026-01-16', needsApproval: false, path: '/accounting/vendors', }, @@ -85,6 +93,7 @@ export const mockData: CEODashboardData = { badge: '결재 요청', content: '출장비 정산 승인 요청 (이영희)', time: '4시간 전', + date: '2026-01-16', needsApproval: true, path: '/approval/inbox', }, @@ -93,6 +102,7 @@ export const mockData: CEODashboardData = { badge: '주식 이슈', content: 'E물류 미수금 8,500,000원 연체 7일', time: '5시간 전', + date: '2026-01-16', needsApproval: false, path: '/accounting/receivables-status', }, @@ -101,6 +111,7 @@ export const mockData: CEODashboardData = { badge: '직정 제고', content: '부품 A-102 재고 부족 경고', time: '6시간 전', + date: '2026-01-16', needsApproval: false, path: '/material/stock-status', }, @@ -109,6 +120,7 @@ export const mockData: CEODashboardData = { badge: '지출예상내역서', content: '장비 구매 품의서 (15,000,000원)', time: '8시간 전', + date: '2026-01-16', needsApproval: true, path: '/approval/inbox', }, @@ -117,6 +129,7 @@ export const mockData: CEODashboardData = { badge: '수주 성공', content: 'F테크 유지보수 계약 연장 85,000,000원', time: '어제', + date: '2026-01-15', needsApproval: false, path: '/sales/order-management-sales', }, @@ -125,6 +138,7 @@ export const mockData: CEODashboardData = { badge: '세금 신고', content: '원천세 신고 완료', time: '어제', + date: '2026-01-15', needsApproval: false, path: '/accounting/tax', }, @@ -133,9 +147,165 @@ export const mockData: CEODashboardData = { badge: '결재 요청', content: '연차 사용 승인 요청 (박지민 외 2명)', time: '어제', + date: '2026-01-15', needsApproval: true, path: '/hr/vacation-management', }, + // 추가 데이터 (스크롤 테스트용) + { + id: 'til16', + badge: '수주 성공', + content: 'G산업 신규 계약 250,000,000원 확정', + time: '2일 전', + date: '2026-01-14', + needsApproval: false, + path: '/sales/order-management-sales', + }, + { + id: 'til17', + badge: '주식 이슈', + content: 'H물류 미수금 12,000,000원 연체 30일', + time: '2일 전', + date: '2026-01-14', + needsApproval: false, + path: '/accounting/receivables-status', + }, + { + id: 'til18', + badge: '직정 제고', + content: '원자재 B-205 안전재고 미달 경고', + time: '2일 전', + date: '2026-01-14', + needsApproval: false, + path: '/material/stock-status', + }, + { + id: 'til19', + badge: '지출예상내역서', + content: '사무용품 구매 품의서 (500,000원)', + time: '2일 전', + date: '2026-01-14', + needsApproval: true, + path: '/approval/inbox', + }, + { + id: 'til20', + badge: '세금 신고', + content: '법인세 중간예납 D-30', + time: '2일 전', + date: '2026-01-14', + needsApproval: false, + path: '/accounting/tax', + }, + { + id: 'til21', + badge: '결재 요청', + content: '해외출장 경비 승인 요청 (최민수)', + time: '3일 전', + date: '2026-01-13', + needsApproval: true, + path: '/approval/inbox', + }, + { + id: 'til22', + badge: '수주 성공', + content: 'I테크 추가 발주 80,000,000원 확정', + time: '3일 전', + date: '2026-01-13', + needsApproval: false, + path: '/sales/order-management-sales', + }, + { + id: 'til23', + badge: '기타', + content: '신규 거래처 J전자 등록 완료', + time: '3일 전', + date: '2026-01-13', + needsApproval: false, + path: '/accounting/vendors', + }, + { + id: 'til24', + badge: '주식 이슈', + content: 'K상사 미수금 5,000,000원 연체 45일', + time: '3일 전', + date: '2026-01-13', + needsApproval: false, + path: '/accounting/receivables-status', + }, + { + id: 'til25', + badge: '직정 제고', + content: '완제품 C-301 재고 부족 경고', + time: '4일 전', + date: '2026-01-12', + needsApproval: false, + path: '/material/stock-status', + }, + { + id: 'til26', + badge: '지출예상내역서', + content: '마케팅 비용 품의서 (3,000,000원)', + time: '4일 전', + date: '2026-01-12', + needsApproval: true, + path: '/approval/inbox', + }, + { + id: 'til27', + badge: '결재 요청', + content: '복리후생비 사용 승인 요청 (정영수)', + time: '4일 전', + date: '2026-01-12', + needsApproval: true, + path: '/approval/inbox', + }, + { + id: 'til28', + badge: '수주 성공', + content: 'L건설 유지보수 계약 연장 45,000,000원', + time: '5일 전', + date: '2026-01-11', + needsApproval: false, + path: '/sales/order-management-sales', + }, + { + id: 'til29', + badge: '기타', + content: '사내 시스템 업데이트 완료', + time: '5일 전', + date: '2026-01-11', + needsApproval: false, + path: '/settings', + }, + { + id: 'til30', + badge: '세금 신고', + content: '지방세 납부 완료', + time: '5일 전', + date: '2026-01-11', + needsApproval: false, + path: '/accounting/tax', + }, + // 1월 6일 (기획서 스크린샷 날짜) 이슈 데이터 + { + id: 'til31', + badge: '직정 제고', + content: '원자재 3종 안전재고 미달', + time: '10일 전', + date: '2026-01-06', + needsApproval: false, + path: '/material/stock-status', + }, + { + id: 'til32', + badge: '결재 요청', + content: '출장비 정산 승인 요청', + time: '10일 전', + date: '2026-01-06', + needsApproval: true, + path: '/approval/inbox', + }, ], dailyReport: { date: '2026년 1월 5일 월요일', diff --git a/src/components/business/CEODashboard/sections/CalendarSection.tsx b/src/components/business/CEODashboard/sections/CalendarSection.tsx index fcc269d4..28bbcb42 100644 --- a/src/components/business/CEODashboard/sections/CalendarSection.tsx +++ b/src/components/business/CEODashboard/sections/CalendarSection.tsx @@ -1,8 +1,10 @@ 'use client'; import { useState, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, @@ -10,7 +12,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Plus } from 'lucide-react'; +import { Plus, ExternalLink } from 'lucide-react'; import { ScheduleCalendar } from '@/components/common/ScheduleCalendar'; import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types'; import type { @@ -18,10 +20,13 @@ import type { CalendarViewType, CalendarDeptFilterType, CalendarTaskFilterType, + TodayIssueListItem, + TodayIssueListBadgeType, } from '../types'; interface CalendarSectionProps { schedules: CalendarScheduleItem[]; + issues?: TodayIssueListItem[]; onScheduleClick?: (schedule: CalendarScheduleItem) => void; onScheduleEdit?: (schedule: CalendarScheduleItem) => void; } @@ -31,9 +36,21 @@ const SCHEDULE_TYPE_COLORS: Record = { schedule: 'blue', order: 'green', construction: 'purple', + issue: 'red', other: 'gray', }; +// 이슈 뱃지별 색상 +const ISSUE_BADGE_COLORS: Record = { + '수주 성공': 'bg-blue-100 text-blue-700', + '주식 이슈': 'bg-purple-100 text-purple-700', + '직정 제고': 'bg-orange-100 text-orange-700', + '지출예상내역서': 'bg-green-100 text-green-700', + '세금 신고': 'bg-red-100 text-red-700', + '결재 요청': 'bg-yellow-100 text-yellow-700', + '기타': 'bg-gray-100 text-gray-700', +}; + // 부서 필터 옵션 const DEPT_FILTER_OPTIONS: { value: CalendarDeptFilterType; label: string }[] = [ { value: 'all', label: '전체' }, @@ -41,27 +58,41 @@ const DEPT_FILTER_OPTIONS: { value: CalendarDeptFilterType; label: string }[] = { value: 'personal', label: '개인' }, ]; -// 업무 필터 옵션 -const TASK_FILTER_OPTIONS: { value: CalendarTaskFilterType; label: string }[] = [ +// 업무 필터 옵션 (이슈 추가) +type ExtendedTaskFilterType = CalendarTaskFilterType | 'issue'; +const TASK_FILTER_OPTIONS: { value: ExtendedTaskFilterType; label: string }[] = [ { value: 'all', label: '전체' }, { value: 'schedule', label: '일정' }, { value: 'order', label: '발주' }, { value: 'construction', label: '시공' }, + { value: 'issue', label: '이슈' }, ]; export function CalendarSection({ schedules, + issues = [], onScheduleClick, onScheduleEdit, }: CalendarSectionProps) { + const router = useRouter(); const [selectedDate, setSelectedDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date()); const [viewType, setViewType] = useState('month'); const [deptFilter, setDeptFilter] = useState('all'); - const [taskFilter, setTaskFilter] = useState('all'); + const [taskFilter, setTaskFilter] = useState('all'); + + // 날짜가 있는 이슈만 필터링 + const issuesWithDate = useMemo(() => { + return issues.filter((issue) => issue.date); + }, [issues]); // 필터링된 스케줄 const filteredSchedules = useMemo(() => { + // 이슈 필터일 경우 스케줄 제외 + if (taskFilter === 'issue') { + return []; + } + let result = schedules; // 업무 필터 @@ -75,31 +106,59 @@ export function CalendarSection({ return result; }, [schedules, taskFilter, deptFilter]); - // ScheduleCalendar용 이벤트 변환 + // 필터링된 이슈 + const filteredIssues = useMemo(() => { + // 이슈 필터가 아니고 all도 아닌 경우 이슈 제외 + if (taskFilter !== 'all' && taskFilter !== 'issue') { + return []; + } + return issuesWithDate; + }, [issuesWithDate, taskFilter]); + + // ScheduleCalendar용 이벤트 변환 (스케줄 + 이슈 통합) const calendarEvents: ScheduleEvent[] = useMemo(() => { - return filteredSchedules.map((schedule) => ({ + const scheduleEvents = filteredSchedules.map((schedule) => ({ id: schedule.id, // 기획서: [부서명] 제목 형식 title: schedule.department ? `[${schedule.department}] ${schedule.title}` : schedule.title, startDate: schedule.startDate, endDate: schedule.endDate, color: SCHEDULE_TYPE_COLORS[schedule.type] || 'gray', - data: schedule, + data: { ...schedule, _type: 'schedule' as const }, })); - }, [filteredSchedules]); - // 선택된 날짜의 일정 목록 - const selectedDateSchedules = useMemo(() => { - if (!selectedDate) return []; + const issueEvents = filteredIssues.map((issue) => ({ + id: issue.id, + title: `[${issue.badge}] ${issue.content}`, + startDate: issue.date!, + endDate: issue.date!, + color: 'red', + data: { ...issue, _type: 'issue' as const }, + })); + + return [...scheduleEvents, ...issueEvents]; + }, [filteredSchedules, filteredIssues]); + + // 선택된 날짜의 일정 + 이슈 목록 + const selectedDateItems = useMemo(() => { + if (!selectedDate) return { schedules: [], issues: [] }; // 로컬 타임존 기준으로 날짜 문자열 생성 (UTC 변환 방지) const year = selectedDate.getFullYear(); const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); const day = String(selectedDate.getDate()).padStart(2, '0'); const dateStr = `${year}-${month}-${day}`; - return filteredSchedules.filter((schedule) => { + + const dateSchedules = filteredSchedules.filter((schedule) => { return schedule.startDate <= dateStr && schedule.endDate >= dateStr; }); - }, [selectedDate, filteredSchedules]); + + const dateIssues = filteredIssues.filter((issue) => issue.date === dateStr); + + return { schedules: dateSchedules, issues: dateIssues }; + }, [selectedDate, filteredSchedules, filteredIssues]); + + // 총 건수 계산 + const totalItemCount = selectedDateItems.schedules.length + selectedDateItems.issues.length; // 날짜 포맷 (기획서: "1월 6일 화요일") const formatSelectedDate = (date: Date) => { @@ -202,13 +261,13 @@ export function CalendarSection({ onDateClick={handleDateClick} onEventClick={handleEventClick} onMonthChange={handleMonthChange} - maxEventsPerDay={2} + maxEventsPerDay={4} weekStartsOn={1} // 월요일 시작 (기획서) className="[&_.weekend]:bg-yellow-50" />

- {/* 선택된 날짜 일정 목록 */} + {/* 선택된 날짜 일정 + 이슈 목록 */}
{/* 헤더: 날짜 + 일정등록 버튼 */}
@@ -242,16 +301,17 @@ export function CalendarSection({ {/* 총 N건 */}
- 총 {selectedDateSchedules.length}건 + 총 {totalItemCount}건
- {selectedDateSchedules.length === 0 ? ( + {totalItemCount === 0 ? (
선택한 날짜에 일정이 없습니다.
) : ( -
- {selectedDateSchedules.map((schedule) => ( +
+ {/* 일정 목록 */} + {selectedDateItems.schedules.map((schedule) => (
))} + + {/* 이슈 목록 */} + {selectedDateItems.issues.map((issue) => ( +
{ + if (issue.path) { + router.push(`/ko${issue.path}`); + } + }} + > + {/* 뱃지 + 제목 */} +
+ + {issue.badge} + + + {issue.content} + +
+ {/* 시간 + 상세보기 */} +
+ {issue.time} + {issue.path && ( + + 상세보기 + + + )} +
+
+ ))}
)}
diff --git a/src/components/business/CEODashboard/sections/TodayIssueSection.tsx b/src/components/business/CEODashboard/sections/TodayIssueSection.tsx index 042a1734..9dae9f72 100644 --- a/src/components/business/CEODashboard/sections/TodayIssueSection.tsx +++ b/src/components/business/CEODashboard/sections/TodayIssueSection.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -26,16 +26,16 @@ const BADGE_COLORS: Record = { '기타': 'bg-gray-100 text-gray-700 hover:bg-gray-100', }; -// 필터 옵션 -const FILTER_OPTIONS = [ - { value: 'all', label: '전체' }, - { value: '수주 성공', label: '수주 성공' }, - { value: '주식 이슈', label: '주식 이슈' }, - { value: '직정 제고', label: '직정 제고' }, - { value: '지출예상내역서', label: '지출예상내역서' }, - { value: '세금 신고', label: '세금 신고' }, - { value: '결재 요청', label: '결재 요청' }, -]; +// 필터 옵션 키 +const FILTER_KEYS = [ + 'all', + '수주 성공', + '주식 이슈', + '직정 제고', + '지출예상내역서', + '세금 신고', + '결재 요청', +] as const; interface TodayIssueSectionProps { items: TodayIssueListItem[]; @@ -49,6 +49,26 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) { // 확인되지 않은 아이템만 필터링 const activeItems = items.filter((item) => !dismissedIds.has(item.id)); + // 항목별 수량 계산 + const itemCounts = useMemo(() => { + const counts: Record = { all: activeItems.length }; + FILTER_KEYS.forEach((key) => { + if (key !== 'all') { + counts[key] = activeItems.filter((item) => item.badge === key).length; + } + }); + return counts; + }, [activeItems]); + + // 필터 옵션 (수량 분리) + const filterOptions = useMemo(() => { + return FILTER_KEYS.map((key) => ({ + value: key, + label: key === 'all' ? '전체' : key, + count: itemCounts[key] || 0, + })); + }, [itemCounts]); + // 필터링된 아이템 const filteredItems = filter === 'all' ? activeItems @@ -83,59 +103,66 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) { {/* 헤더 */} -
+

오늘의 이슈

- {/* 리스트 */} -
+ {/* 리스트 - 반응형 그리드 (4열 → 1열) */} +
{filteredItems.length === 0 ? ( -
+
표시할 이슈가 없습니다.
) : ( filteredItems.map((item) => (
handleItemClick(item)} > - {/* 좌측: 뱃지 + 내용 */} -
- - {item.badge} - - - {item.content} - -
+ {/* 뱃지 */} + + {item.badge} + - {/* 우측: 시간 + 버튼 */} -
e.stopPropagation()}> - - {item.time} - + {/* 내용 */} + + {item.content} + + + {/* 시간 */} + + {item.time} + + + {/* 버튼 */} +
e.stopPropagation()}> {item.needsApproval ? ( -
+ <> -
+ ) : (