fix(WEB): 오늘의 이슈 notification_type 기반 변환 로직 개선
transformers.ts: - notification_type 영문 코드 기반 변환 (sales_order, bad_debt 등) - NOTIFICATION_TYPE_TO_BADGE 매핑 테이블 추가 - validateNotificationType 함수로 타입 안전성 확보 types.ts: - TodayIssueNotificationType 타입 추가 - TodayIssueListItem에 notificationType 필드 추가 TodayIssueSection: - notificationType 기반 색상 매핑 적용 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -486,7 +486,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
) : (
|
||||
<CheckCircle2 style={{ color: color.iconColor }} className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
)}
|
||||
<p style={{ color: '#475569' }} className="text-xs line-clamp-2">{cp.message}</p>
|
||||
<p style={{ color: '#475569' }} className="text-sm">{cp.message}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -26,9 +26,9 @@ import {
|
||||
Info,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import type { TodayIssueListItem, TodayIssueListBadgeType } from '../types';
|
||||
import type { TodayIssueListItem, TodayIssueNotificationType } from '../types';
|
||||
|
||||
// 뱃지 스타일 매핑 (아이콘 + 색상)
|
||||
// 뱃지 스타일 매핑 (notification_type 코드 기반)
|
||||
interface BadgeStyle {
|
||||
bg: string;
|
||||
text: string;
|
||||
@@ -36,31 +36,18 @@ interface BadgeStyle {
|
||||
Icon: LucideIcon;
|
||||
}
|
||||
|
||||
const BADGE_STYLES: Record<TodayIssueListBadgeType, BadgeStyle> = {
|
||||
'수주 성공': { bg: 'bg-blue-50', text: 'text-blue-700', iconBg: 'bg-blue-500', Icon: ShoppingCart },
|
||||
'추심 이슈': { bg: 'bg-purple-50', text: 'text-purple-700', iconBg: 'bg-purple-500', Icon: AlertCircle },
|
||||
'적정 재고': { bg: 'bg-orange-50', text: 'text-orange-700', iconBg: 'bg-orange-500', Icon: Package },
|
||||
'지출예상내역서': { bg: 'bg-green-50', text: 'text-green-700', iconBg: 'bg-green-500', Icon: Receipt },
|
||||
'세금 신고': { bg: 'bg-red-50', text: 'text-red-700', iconBg: 'bg-red-500', Icon: FileText },
|
||||
'결재 요청': { bg: 'bg-amber-50', text: 'text-amber-700', iconBg: 'bg-amber-500', Icon: CheckCircle2 },
|
||||
'신규거래처': { bg: 'bg-emerald-50', text: 'text-emerald-700', iconBg: 'bg-emerald-500', Icon: Building2 },
|
||||
'입금': { bg: 'bg-cyan-50', text: 'text-cyan-700', iconBg: 'bg-cyan-500', Icon: TrendingUp },
|
||||
'출금': { bg: 'bg-pink-50', text: 'text-pink-700', iconBg: 'bg-pink-500', Icon: TrendingDown },
|
||||
'기타': { bg: 'bg-gray-50', text: 'text-gray-700', iconBg: 'bg-gray-500', Icon: Info },
|
||||
};
|
||||
|
||||
// 기존 호환용 뱃지 색상 (legacy)
|
||||
const BADGE_COLORS: Record<TodayIssueListBadgeType, string> = {
|
||||
'수주 성공': 'bg-blue-100 text-blue-700 hover:bg-blue-100',
|
||||
'추심 이슈': 'bg-purple-100 text-purple-700 hover:bg-purple-100',
|
||||
'적정 재고': 'bg-orange-100 text-orange-700 hover:bg-orange-100',
|
||||
'지출예상내역서': 'bg-green-100 text-green-700 hover:bg-green-100',
|
||||
'세금 신고': 'bg-red-100 text-red-700 hover:bg-red-100',
|
||||
'결재 요청': 'bg-yellow-100 text-yellow-700 hover:bg-yellow-100',
|
||||
'신규거래처': 'bg-emerald-100 text-emerald-700 hover:bg-emerald-100',
|
||||
'입금': 'bg-cyan-100 text-cyan-700 hover:bg-cyan-100',
|
||||
'출금': 'bg-pink-100 text-pink-700 hover:bg-pink-100',
|
||||
'기타': 'bg-gray-100 text-gray-700 hover:bg-gray-100',
|
||||
// notification_type 코드 기반 스타일 매핑 (API 고정값 사용)
|
||||
const NOTIFICATION_STYLES: Record<TodayIssueNotificationType, BadgeStyle> = {
|
||||
sales_order: { bg: 'bg-blue-50', text: 'text-blue-700', iconBg: 'bg-blue-500', Icon: ShoppingCart },
|
||||
bad_debt: { bg: 'bg-purple-50', text: 'text-purple-700', iconBg: 'bg-purple-500', Icon: AlertCircle },
|
||||
safety_stock: { bg: 'bg-orange-50', text: 'text-orange-700', iconBg: 'bg-orange-500', Icon: Package },
|
||||
expected_expense: { bg: 'bg-green-50', text: 'text-green-700', iconBg: 'bg-green-500', Icon: Receipt },
|
||||
vat_report: { bg: 'bg-red-50', text: 'text-red-700', iconBg: 'bg-red-500', Icon: FileText },
|
||||
approval_request: { bg: 'bg-amber-50', text: 'text-amber-700', iconBg: 'bg-amber-500', Icon: CheckCircle2 },
|
||||
new_vendor: { bg: 'bg-emerald-50', text: 'text-emerald-700', iconBg: 'bg-emerald-500', Icon: Building2 },
|
||||
deposit: { bg: 'bg-cyan-50', text: 'text-cyan-700', iconBg: 'bg-cyan-500', Icon: TrendingUp },
|
||||
withdrawal: { bg: 'bg-pink-50', text: 'text-pink-700', iconBg: 'bg-pink-500', Icon: TrendingDown },
|
||||
other: { bg: 'bg-gray-50', text: 'text-gray-700', iconBg: 'bg-gray-500', Icon: Info },
|
||||
};
|
||||
|
||||
// 신용등급 색상 매핑 (A=녹색, B=노랑, C=주황, D=빨강)
|
||||
@@ -78,25 +65,33 @@ const getRandomCreditRating = (): CreditRating => {
|
||||
return ratings[Math.floor(Math.random() * ratings.length)];
|
||||
};
|
||||
|
||||
// 필터 옵션 키 (API TodayIssue 모델과 동기화)
|
||||
// 필터 옵션 키 (notification_type 코드 기반)
|
||||
const FILTER_KEYS = [
|
||||
'all',
|
||||
'수주 성공',
|
||||
'추심 이슈',
|
||||
'적정 재고',
|
||||
'지출예상내역서',
|
||||
'세금 신고',
|
||||
'결재 요청',
|
||||
'신규거래처',
|
||||
'입금',
|
||||
'출금',
|
||||
'기타',
|
||||
'sales_order',
|
||||
'bad_debt',
|
||||
'safety_stock',
|
||||
'expected_expense',
|
||||
'vat_report',
|
||||
'approval_request',
|
||||
'new_vendor',
|
||||
'deposit',
|
||||
'withdrawal',
|
||||
'other',
|
||||
] as const;
|
||||
|
||||
// badge를 필터 키로 변환 (정의되지 않은 타입은 기타로)
|
||||
const getFilterKey = (badge: string): string => {
|
||||
const knownBadges = ['수주 성공', '추심 이슈', '적정 재고', '지출예상내역서', '세금 신고', '결재 요청', '신규거래처', '입금', '출금'];
|
||||
return knownBadges.includes(badge) ? badge : '기타';
|
||||
// notification_type → 한글 라벨 매핑 (필터 표시용)
|
||||
const NOTIFICATION_TYPE_LABELS: Record<TodayIssueNotificationType, string> = {
|
||||
sales_order: '수주 성공',
|
||||
bad_debt: '추심 이슈',
|
||||
safety_stock: '적정 재고',
|
||||
expected_expense: '지출예상내역서',
|
||||
vat_report: '세금 신고',
|
||||
approval_request: '결재 요청',
|
||||
new_vendor: '신규거래처',
|
||||
deposit: '입금',
|
||||
withdrawal: '출금',
|
||||
other: '기타',
|
||||
};
|
||||
|
||||
interface TodayIssueSectionProps {
|
||||
@@ -115,37 +110,37 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
const creditRatings = useMemo(() => {
|
||||
const ratings: Record<string, CreditRating> = {};
|
||||
items.forEach((item) => {
|
||||
if (item.badge === '신규거래처') {
|
||||
if (item.notificationType === 'new_vendor') {
|
||||
ratings[item.id] = getRandomCreditRating();
|
||||
}
|
||||
});
|
||||
return ratings;
|
||||
}, [items]);
|
||||
|
||||
// 항목별 수량 계산 (입금/출금 등은 기타로 카운트)
|
||||
// 항목별 수량 계산 (notification_type 코드 기반)
|
||||
const itemCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = { all: activeItems.length };
|
||||
FILTER_KEYS.forEach((key) => {
|
||||
if (key !== 'all') {
|
||||
counts[key] = activeItems.filter((item) => getFilterKey(item.badge) === key).length;
|
||||
counts[key] = activeItems.filter((item) => item.notificationType === key).length;
|
||||
}
|
||||
});
|
||||
return counts;
|
||||
}, [activeItems]);
|
||||
|
||||
// 필터 옵션 (수량 분리)
|
||||
// 필터 옵션 (notification_type 코드 기반, 한글 라벨 표시)
|
||||
const filterOptions = useMemo(() => {
|
||||
return FILTER_KEYS.map((key) => ({
|
||||
value: key,
|
||||
label: key === 'all' ? '전체' : key,
|
||||
label: key === 'all' ? '전체' : NOTIFICATION_TYPE_LABELS[key as TodayIssueNotificationType],
|
||||
count: itemCounts[key] || 0,
|
||||
}));
|
||||
}, [itemCounts]);
|
||||
|
||||
// 필터링된 아이템 (입금/출금 등은 기타로 분류)
|
||||
// 필터링된 아이템 (notification_type 코드 기반)
|
||||
const filteredItems = filter === 'all'
|
||||
? activeItems
|
||||
: activeItems.filter((item) => getFilterKey(item.badge) === filter);
|
||||
: activeItems.filter((item) => item.notificationType === filter);
|
||||
|
||||
// 아이템 클릭
|
||||
const handleItemClick = (item: TodayIssueListItem) => {
|
||||
@@ -205,8 +200,11 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
</div>
|
||||
) : (
|
||||
filteredItems.map((item) => {
|
||||
const badgeStyle = BADGE_STYLES[item.badge as TodayIssueListBadgeType] || BADGE_STYLES['기타'];
|
||||
// notification_type 코드 기반 스타일 매핑 (안정적)
|
||||
const badgeStyle = NOTIFICATION_STYLES[item.notificationType] || NOTIFICATION_STYLES.other;
|
||||
const BadgeIcon = badgeStyle.Icon;
|
||||
// 표시용 한글 라벨
|
||||
const displayLabel = NOTIFICATION_TYPE_LABELS[item.notificationType] || item.badge;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -220,7 +218,7 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
<BadgeIcon className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<span className={`text-xs font-semibold ${badgeStyle.text}`}>
|
||||
{item.badge}
|
||||
{displayLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -230,7 +228,7 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
</span>
|
||||
|
||||
{/* 신용등급 배지 (신규업체인 경우) */}
|
||||
{item.badge === '신규업체' && creditRatings[item.id] && (
|
||||
{item.notificationType === 'new_vendor' && creditRatings[item.id] && (
|
||||
<Button
|
||||
size="sm"
|
||||
className={`h-6 px-2 text-xs shrink-0 ${CREDIT_RATING_COLORS[creditRatings[item.id]]}`}
|
||||
|
||||
@@ -56,7 +56,20 @@ export interface TodayIssueItem {
|
||||
icon?: React.ComponentType<{ className?: string }>; // 카드 아이콘
|
||||
}
|
||||
|
||||
// 오늘의 이슈 뱃지 타입 (API TodayIssue 모델과 동기화)
|
||||
// 오늘의 이슈 notification_type 코드 (API 고정값 - 색상 매핑용)
|
||||
export type TodayIssueNotificationType =
|
||||
| 'sales_order' // 수주등록
|
||||
| 'bad_debt' // 추심이슈
|
||||
| 'safety_stock' // 안전재고
|
||||
| 'expected_expense' // 지출 승인대기
|
||||
| 'vat_report' // 세금 신고
|
||||
| 'approval_request' // 결재 요청
|
||||
| 'new_vendor' // 신규거래처
|
||||
| 'deposit' // 입금
|
||||
| 'withdrawal' // 출금
|
||||
| 'other'; // 기타
|
||||
|
||||
// 오늘의 이슈 뱃지 타입 (한글 표시용 - deprecated, notificationType 사용 권장)
|
||||
export type TodayIssueListBadgeType =
|
||||
| '수주 성공'
|
||||
| '추심 이슈'
|
||||
@@ -72,7 +85,8 @@ export type TodayIssueListBadgeType =
|
||||
// 오늘의 이슈 리스트 아이템 (리스트 형태 - 새로운 오늘의 이슈용)
|
||||
export interface TodayIssueListItem {
|
||||
id: string;
|
||||
badge: TodayIssueListBadgeType;
|
||||
badge: TodayIssueListBadgeType; // 한글 표시용
|
||||
notificationType: TodayIssueNotificationType; // 색상 매핑용 코드값
|
||||
content: string;
|
||||
time: string; // "10분 전", "1시간 전" 등
|
||||
date?: string; // ISO date string (캘린더 표시용)
|
||||
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
TodayIssueItem,
|
||||
TodayIssueListItem,
|
||||
TodayIssueListBadgeType,
|
||||
TodayIssueNotificationType,
|
||||
CalendarScheduleItem,
|
||||
CheckPoint,
|
||||
CheckPointType,
|
||||
@@ -602,49 +603,71 @@ export function transformStatusBoardResponse(api: StatusBoardApiResponse): Today
|
||||
// 7. TodayIssue 변환
|
||||
// ============================================
|
||||
|
||||
/** 유효한 뱃지 타입 목록 (API TodayIssue 모델과 동기화) */
|
||||
const VALID_BADGE_TYPES: TodayIssueListBadgeType[] = [
|
||||
'수주등록',
|
||||
'추심이슈',
|
||||
'안전재고',
|
||||
'지출승인',
|
||||
'세금신고',
|
||||
'결재요청',
|
||||
'신규업체',
|
||||
'입금',
|
||||
'출금',
|
||||
'기타',
|
||||
/** 유효한 notification_type 목록 (API TodayIssue 모델과 동기화) */
|
||||
const VALID_NOTIFICATION_TYPES: TodayIssueNotificationType[] = [
|
||||
'sales_order',
|
||||
'bad_debt',
|
||||
'safety_stock',
|
||||
'expected_expense',
|
||||
'vat_report',
|
||||
'approval_request',
|
||||
'new_vendor',
|
||||
'deposit',
|
||||
'withdrawal',
|
||||
'other',
|
||||
];
|
||||
|
||||
/** notification_type → 한글 badge 변환 매핑 */
|
||||
const NOTIFICATION_TYPE_TO_BADGE: Record<TodayIssueNotificationType, TodayIssueListBadgeType> = {
|
||||
sales_order: '수주 성공',
|
||||
bad_debt: '추심 이슈',
|
||||
safety_stock: '적정 재고',
|
||||
expected_expense: '지출예상내역서',
|
||||
vat_report: '세금 신고',
|
||||
approval_request: '결재 요청',
|
||||
new_vendor: '신규거래처',
|
||||
deposit: '입금',
|
||||
withdrawal: '출금',
|
||||
other: '기타',
|
||||
};
|
||||
|
||||
/**
|
||||
* API 뱃지 문자열 → Frontend 뱃지 타입 변환
|
||||
* 유효하지 않은 뱃지는 '기타'로 폴백
|
||||
* API notification_type → Frontend notificationType 변환
|
||||
* 유효하지 않은 타입은 'other'로 폴백
|
||||
*/
|
||||
function validateBadgeType(badge: string): TodayIssueListBadgeType {
|
||||
if (VALID_BADGE_TYPES.includes(badge as TodayIssueListBadgeType)) {
|
||||
return badge as TodayIssueListBadgeType;
|
||||
function validateNotificationType(notificationType: string | null): TodayIssueNotificationType {
|
||||
if (notificationType && VALID_NOTIFICATION_TYPES.includes(notificationType as TodayIssueNotificationType)) {
|
||||
return notificationType as TodayIssueNotificationType;
|
||||
}
|
||||
return '기타';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* TodayIssue API 응답 → Frontend 타입 변환
|
||||
* 오늘의 이슈 리스트 데이터 변환
|
||||
* notification_type 코드값 기반으로 색상 매핑 지원
|
||||
*/
|
||||
export function transformTodayIssueResponse(api: TodayIssueApiResponse): {
|
||||
items: TodayIssueListItem[];
|
||||
totalCount: number;
|
||||
} {
|
||||
return {
|
||||
items: api.items.map((item) => ({
|
||||
id: item.id,
|
||||
badge: validateBadgeType(item.badge),
|
||||
content: item.content,
|
||||
time: item.time,
|
||||
date: item.date,
|
||||
needsApproval: item.needsApproval ?? false,
|
||||
path: normalizePath(item.path, { addViewMode: true }),
|
||||
})),
|
||||
items: api.items.map((item) => {
|
||||
const notificationType = validateNotificationType(item.notification_type);
|
||||
// badge는 API 응답 그대로 사용하되, 없으면 notification_type에서 변환
|
||||
const badge = item.badge || NOTIFICATION_TYPE_TO_BADGE[notificationType];
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
badge: badge as TodayIssueListBadgeType,
|
||||
notificationType,
|
||||
content: item.content,
|
||||
time: item.time,
|
||||
date: item.date,
|
||||
needsApproval: item.needsApproval ?? false,
|
||||
path: normalizePath(item.path, { addViewMode: true }),
|
||||
};
|
||||
}),
|
||||
totalCount: api.total_count,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,10 +173,23 @@ export interface StatusBoardApiResponse {
|
||||
// 7. TodayIssue (오늘의 이슈 리스트) API 응답 타입
|
||||
// ============================================
|
||||
|
||||
/** 오늘의 이슈 notification_type 코드 (API 고정값) */
|
||||
export type TodayIssueNotificationType =
|
||||
| 'sales_order' // 수주등록
|
||||
| 'bad_debt' // 추심이슈
|
||||
| 'safety_stock' // 안전재고
|
||||
| 'expected_expense' // 지출 승인대기
|
||||
| 'vat_report' // 세금 신고
|
||||
| 'approval_request' // 결재 요청
|
||||
| 'new_vendor' // 신규거래처
|
||||
| 'deposit' // 입금
|
||||
| 'withdrawal'; // 출금
|
||||
|
||||
/** 오늘의 이슈 아이템 */
|
||||
export interface TodayIssueItemApiResponse {
|
||||
id: string; // 항목 고유 ID
|
||||
badge: string; // 이슈 카테고리 뱃지
|
||||
badge: string; // 이슈 카테고리 뱃지 (한글)
|
||||
notification_type: TodayIssueNotificationType | null; // 이슈 타입 코드 (영문)
|
||||
content: string; // 이슈 내용
|
||||
time: string; // 상대 시간 (예: "10분 전")
|
||||
date?: string; // 날짜 (ISO 형식)
|
||||
|
||||
Reference in New Issue
Block a user