feat: 신규 페이지 구현 및 HR/설정 기능 개선

신규 페이지:
- 회계관리: 거래처, 예상비용, 청구서, 발주서
- 게시판: 공지사항, 자료실, 커뮤니티
- 고객센터: 문의/FAQ
- 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역
- 리포트 (차트 시각화)
- 개발자 테스트 URL 페이지

기능 개선:
- HR 직원관리/휴가관리/카드관리 강화
- IntegratedListTemplateV2 확장
- AuthenticatedLayout 패딩 표준화
- 로그인 페이지 UI 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-19 19:12:34 +09:00
parent d742c0ce26
commit c6b605200d
213 changed files with 32644 additions and 775 deletions

View File

@@ -0,0 +1,221 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { ClipboardList, ArrowLeft, Save } from 'lucide-react';
import type { Board, BoardFormData, BoardTarget, BoardStatus } from './types';
import { BOARD_TARGETS, MOCK_DEPARTMENTS, BOARD_STATUS_LABELS } from './types';
interface BoardFormProps {
mode: 'create' | 'edit';
board?: Board;
onSubmit: (data: BoardFormData) => void;
}
// 날짜/시간 포맷
const formatDateTime = (dateString: string): string => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
// 현재 날짜/시간
const getCurrentDateTime = (): string => {
return formatDateTime(new Date().toISOString());
};
export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
const router = useRouter();
const [formData, setFormData] = useState<BoardFormData>({
target: 'all',
targetName: '',
boardName: '',
status: 'active',
});
// 수정 모드일 때 기존 데이터 로드
useEffect(() => {
if (mode === 'edit' && board) {
setFormData({
target: board.target,
targetName: board.targetName || '',
boardName: board.boardName,
status: board.status,
});
}
}, [mode, board]);
const handleBack = () => {
if (mode === 'edit' && board) {
router.push(`/ko/board/board-management/${board.id}`);
} else {
router.push('/ko/board/board-management');
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};
const handleTargetChange = (value: BoardTarget) => {
setFormData(prev => ({
...prev,
target: value,
targetName: value === 'all' ? '' : prev.targetName,
}));
};
// 작성자 (현재 로그인한 사용자 - mock)
const currentUser = '홍길동';
// 등록일시
const registeredAt = mode === 'edit' && board ? formatDateTime(board.createdAt) : getCurrentDateTime();
return (
<PageLayout>
<PageHeader
title={mode === 'create' ? '게시판관리 상세' : '게시판관리 상세'}
description="게시판 목록을 관리합니다"
icon={ClipboardList}
/>
<form onSubmit={handleSubmit} className="space-y-6">
{/* 게시판 정보 */}
<Card>
<CardHeader>
<CardTitle className="text-base"> *</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 대상 */}
<div className="space-y-2">
<Label htmlFor="target"></Label>
<div className="flex gap-2">
<Select
value={formData.target}
onValueChange={(value) => handleTargetChange(value as BoardTarget)}
>
<SelectTrigger id="target" className="w-[120px]">
<SelectValue placeholder="대상 선택" />
</SelectTrigger>
<SelectContent>
{BOARD_TARGETS.map((target) => (
<SelectItem key={target.value} value={target.value}>
{target.label}
</SelectItem>
))}
</SelectContent>
</Select>
{formData.target === 'department' && (
<Select
value={formData.targetName}
onValueChange={(value) => setFormData(prev => ({ ...prev, targetName: value }))}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="부서 선택" />
</SelectTrigger>
<SelectContent>
{MOCK_DEPARTMENTS.map((dept) => (
<SelectItem key={dept.id} value={dept.name}>
{dept.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
</div>
{/* 작성자 */}
<div className="space-y-2">
<Label htmlFor="author"></Label>
<Input
id="author"
value={mode === 'edit' && board ? board.authorName : currentUser}
disabled
className="bg-muted"
/>
</div>
{/* 게시판명 */}
<div className="space-y-2 md:col-span-2">
<Label htmlFor="boardName"></Label>
<Input
id="boardName"
value={formData.boardName}
onChange={(e) => setFormData(prev => ({ ...prev, boardName: e.target.value }))}
placeholder="게시판명을 입력해주세요"
/>
</div>
{/* 상태 */}
<div className="space-y-2">
<Label></Label>
<RadioGroup
value={formData.status}
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value as BoardStatus }))}
className="flex items-center gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="inactive" id="inactive" />
<Label htmlFor="inactive" className="font-normal cursor-pointer">
{BOARD_STATUS_LABELS.inactive}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="active" id="active" />
<Label htmlFor="active" className="font-normal cursor-pointer">
{BOARD_STATUS_LABELS.active}
</Label>
</div>
</RadioGroup>
</div>
{/* 등록일시 */}
<div className="space-y-2">
<Label htmlFor="registeredAt"></Label>
<Input
id="registeredAt"
value={registeredAt}
disabled
className="bg-muted"
/>
</div>
</div>
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="flex items-center justify-between">
<Button type="button" variant="outline" onClick={handleBack}>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<Button type="submit">
<Save className="w-4 h-4 mr-2" />
{mode === 'create' ? '등록' : '저장'}
</Button>
</div>
</form>
</PageLayout>
);
}