feat: 공지 팝업 시스템 구현 및 캘린더/어음/팝업관리 개선

- NoticePopupModal: 공지 팝업 컨테이너/actions 신규 구현
- AuthenticatedLayout에 공지 팝업 연동
- CalendarSection: 일정 타입 확장 및 UI 개선
- BillManagementClient: 기능 확장
- PopupManagement: popupDetailConfig 대폭 확장, 상세/폼 개선
- BoardForm/BoardManagement: 게시판 폼 개선
- LoginPage, logout, userStorage: 인증 관련 소폭 수정
- dashboard types 정비
- claudedocs: 공지팝업 구현, 캘린더 어음연동/일정타입, API changelog 문서 추가
This commit is contained in:
유병철
2026-03-10 15:16:41 +09:00
parent 7bd4bd38da
commit 397eb2c19c
23 changed files with 1004 additions and 79 deletions

View File

@@ -17,7 +17,7 @@ import { useDateRange } from '@/hooks';
import {
FileText,
Plus,
Save,
RefreshCw,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
@@ -51,6 +51,8 @@ import {
BILL_TYPE_FILTER_OPTIONS,
BILL_STATUS_COLORS,
BILL_STATUS_FILTER_OPTIONS,
RECEIVED_BILL_STATUS_OPTIONS,
ISSUED_BILL_STATUS_OPTIONS,
getBillStatusLabel,
} from './types';
import { getBills, deleteBill, updateBillStatus } from './actions';
@@ -84,6 +86,7 @@ export function BillManagementClient({
const [billTypeFilter, setBillTypeFilter] = useState<string>(initialBillType || 'received');
const [vendorFilter, setVendorFilter] = useState<string>(initialVendorId || 'all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [targetStatus, setTargetStatus] = useState<string>('');
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [currentPage, setCurrentPage] = useState(initialPagination.currentPage);
const itemsPerPage = initialPagination.perPage;
@@ -262,15 +265,15 @@ export function BillManagementClient({
];
}, [data]);
// ===== 저장 핸들러 =====
const handleSave = useCallback(async () => {
// ===== 상태 변경 핸들러 =====
const handleStatusChange = useCallback(async () => {
if (selectedItems.size === 0) {
toast.warning('선택된 항목이 없습니다.');
return;
}
if (statusFilter === 'all') {
toast.warning('상태를 선택해주세요.');
if (!targetStatus) {
toast.warning('변경할 상태를 선택해주세요.');
return;
}
@@ -278,7 +281,7 @@ export function BillManagementClient({
let successCount = 0;
for (const id of selectedItems) {
const result = await updateBillStatus(id, statusFilter as BillStatus);
const result = await updateBillStatus(id, targetStatus as BillStatus);
if (result.success) {
successCount++;
}
@@ -286,14 +289,20 @@ export function BillManagementClient({
if (successCount > 0) {
invalidateDashboard('bill');
toast.success(`${successCount}이 저장되었습니다.`);
toast.success(`${successCount}의 상태가 변경되었습니다.`);
loadData(currentPage);
setSelectedItems(new Set());
setTargetStatus('');
} else {
toast.error('저장에 실패했습니다.');
toast.error('상태 변경에 실패했습니다.');
}
setIsLoading(false);
}, [selectedItems, statusFilter, loadData, currentPage]);
}, [selectedItems, targetStatus, loadData, currentPage]);
// 구분에 따른 상태 옵션
const statusChangeOptions = useMemo(() => {
return billTypeFilter === 'issued' ? ISSUED_BILL_STATUS_OPTIONS : RECEIVED_BILL_STATUS_OPTIONS;
}, [billTypeFilter]);
// ===== UniversalListPage Config =====
const config: UniversalListConfig<BillRecord> = useMemo(
@@ -377,12 +386,30 @@ export function BillManagementClient({
icon: Plus,
},
// 헤더 액션: 저장 버튼만 (필터는 tableHeaderActions에서 통합 관리)
headerActions: () => (
<Button onClick={handleSave} size="sm" disabled={isLoading}>
<Save className="h-4 w-4 mr-1" />
</Button>
// 선택 시 상태 변경 액션
selectionActions: () => (
<div className="flex items-center gap-2">
<Select value={targetStatus} onValueChange={setTargetStatus}>
<SelectTrigger className="min-w-[130px] w-auto h-8">
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
{statusChangeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
onClick={handleStatusChange}
disabled={!targetStatus || isLoading}
>
<RefreshCw className="h-4 w-4 mr-1" />
</Button>
</div>
),
// 테이블 헤더 액션 (필터)
@@ -447,7 +474,9 @@ export function BillManagementClient({
router,
loadData,
currentPage,
handleSave,
handleStatusChange,
statusChangeOptions,
targetStatus,
renderTableRow,
renderMobileCard,
]