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:
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
|
||||
export default function EditBadDebtPage() {
|
||||
const params = useParams();
|
||||
const recordId = params.id as string;
|
||||
|
||||
return <BadDebtDetail mode="edit" recordId={recordId} />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
|
||||
export default function BadDebtDetailPage() {
|
||||
const params = useParams();
|
||||
const recordId = params.id as string;
|
||||
|
||||
return <BadDebtDetail mode="view" recordId={recordId} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
|
||||
export default function NewBadDebtPage() {
|
||||
return <BadDebtDetail mode="new" />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BadDebtCollection } from '@/components/accounting/BadDebtCollection';
|
||||
|
||||
export default function BadDebtCollectionPage() {
|
||||
return <BadDebtCollection />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BankTransactionInquiry } from '@/components/accounting/BankTransactionInquiry';
|
||||
|
||||
export default function BankTransactionsPage() {
|
||||
return <BankTransactionInquiry />;
|
||||
}
|
||||
13
src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx
Normal file
13
src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { BillDetail } from '@/components/accounting/BillManagement/BillDetail';
|
||||
|
||||
export default function BillDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const billId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <BillDetail billId={billId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BillDetail } from '@/components/accounting/BillManagement/BillDetail';
|
||||
|
||||
export default function BillNewPage() {
|
||||
return <BillDetail billId="new" mode="new" />;
|
||||
}
|
||||
12
src/app/[locale]/(protected)/accounting/bills/page.tsx
Normal file
12
src/app/[locale]/(protected)/accounting/bills/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { BillManagement } from '@/components/accounting/BillManagement';
|
||||
|
||||
export default function BillsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const vendorId = searchParams.get('vendorId') || undefined;
|
||||
const billType = searchParams.get('type') || undefined;
|
||||
|
||||
return <BillManagement initialVendorId={vendorId} initialBillType={billType} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CardTransactionInquiry } from '@/components/accounting/CardTransactionInquiry';
|
||||
|
||||
export default function CardTransactionsPage() {
|
||||
return <CardTransactionInquiry />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { DailyReport } from '@/components/accounting/DailyReport';
|
||||
|
||||
export default function DailyReportPage() {
|
||||
return <DailyReport />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { DepositDetail } from '@/components/accounting/DepositManagement/DepositDetail';
|
||||
|
||||
export default function DepositDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const depositId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <DepositDetail depositId={depositId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { DepositManagement } from '@/components/accounting/DepositManagement';
|
||||
|
||||
export default function DepositsPage() {
|
||||
return <DepositManagement />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ExpectedExpenseManagement } from '@/components/accounting/ExpectedExpenseManagement';
|
||||
|
||||
export default function ExpectedExpensesPage() {
|
||||
return <ExpectedExpenseManagement />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { PurchaseDetail } from '@/components/accounting/PurchaseManagement/PurchaseDetail';
|
||||
|
||||
export default function PurchaseDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const purchaseId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <PurchaseDetail purchaseId={purchaseId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PurchaseManagement } from '@/components/accounting/PurchaseManagement';
|
||||
|
||||
export default function PurchasePage() {
|
||||
return <PurchaseManagement />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ReceivablesStatus } from '@/components/accounting/ReceivablesStatus';
|
||||
|
||||
export default function ReceivablesStatusPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const highlightVendorId = searchParams.get('highlight') || undefined;
|
||||
|
||||
return <ReceivablesStatus highlightVendorId={highlightVendorId} />;
|
||||
}
|
||||
13
src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx
Normal file
13
src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
|
||||
|
||||
export default function SalesDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const salesId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <SalesDetail mode={mode} salesId={salesId} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
|
||||
|
||||
export default function NewSalesPage() {
|
||||
return <SalesDetail mode="new" />;
|
||||
}
|
||||
5
src/app/[locale]/(protected)/accounting/sales/page.tsx
Normal file
5
src/app/[locale]/(protected)/accounting/sales/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SalesManagement } from '@/components/accounting/SalesManagement';
|
||||
|
||||
export default function SalesPage() {
|
||||
return <SalesManagement />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { VendorLedgerDetail } from '@/components/accounting/VendorLedger/VendorLedgerDetail';
|
||||
|
||||
export default function VendorLedgerDetailPage() {
|
||||
const params = useParams();
|
||||
const vendorId = params.id as string;
|
||||
|
||||
return <VendorLedgerDetail vendorId={vendorId} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VendorLedger } from '@/components/accounting/VendorLedger';
|
||||
|
||||
export default function VendorLedgerPage() {
|
||||
return <VendorLedger />;
|
||||
}
|
||||
13
src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx
vendored
Normal file
13
src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail';
|
||||
|
||||
export default function VendorDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const vendorId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <VendorDetail mode={mode} vendorId={vendorId} />;
|
||||
}
|
||||
7
src/app/[locale]/(protected)/accounting/vendors/new/page.tsx
vendored
Normal file
7
src/app/[locale]/(protected)/accounting/vendors/new/page.tsx
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail';
|
||||
|
||||
export default function NewVendorPage() {
|
||||
return <VendorDetail mode="new" />;
|
||||
}
|
||||
7
src/app/[locale]/(protected)/accounting/vendors/page.tsx
vendored
Normal file
7
src/app/[locale]/(protected)/accounting/vendors/page.tsx
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VendorManagement } from '@/components/accounting/VendorManagement';
|
||||
|
||||
export default function VendorsPage() {
|
||||
return <VendorManagement />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { WithdrawalDetail } from '@/components/accounting/WithdrawalManagement/WithdrawalDetail';
|
||||
|
||||
export default function WithdrawalDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const withdrawalId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <WithdrawalDetail withdrawalId={withdrawalId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement';
|
||||
|
||||
export default function WithdrawalsPage() {
|
||||
return <WithdrawalManagement />;
|
||||
}
|
||||
57
src/app/[locale]/(protected)/board/[id]/edit/page.tsx
Normal file
57
src/app/[locale]/(protected)/board/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 게시글 수정 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { BoardForm } from '@/components/board/BoardForm';
|
||||
import type { Post } from '@/components/board/types';
|
||||
import { MOCK_BOARDS } from '@/components/board/types';
|
||||
|
||||
// Mock 데이터 생성 (실제로는 API에서 가져옴)
|
||||
const generateMockPost = (id: string): Post => {
|
||||
const boards = MOCK_BOARDS.filter((b) => b.id !== 'all');
|
||||
const board = boards[0];
|
||||
|
||||
return {
|
||||
id,
|
||||
boardId: board.id,
|
||||
boardName: board.name,
|
||||
title: '제목',
|
||||
content: `
|
||||
<p>게시글 내용입니다.</p>
|
||||
<p>이것은 <strong>테스트용</strong> 콘텐츠입니다.</p>
|
||||
`,
|
||||
authorId: 'user1',
|
||||
authorName: '홍길동',
|
||||
authorDepartment: '개발팀',
|
||||
authorPosition: '과장',
|
||||
isPinned: false,
|
||||
allowComments: true,
|
||||
viewCount: 123,
|
||||
attachments: [
|
||||
{
|
||||
id: 'file-1',
|
||||
fileName: 'abc.pdf',
|
||||
fileSize: 1024000,
|
||||
fileUrl: '/files/abc.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
},
|
||||
],
|
||||
createdAt: format(new Date(2025, 8, 9, 12, 20), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 9, 12, 20), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
};
|
||||
};
|
||||
|
||||
export default function BoardEditPage() {
|
||||
const params = useParams();
|
||||
const postId = params.id as string;
|
||||
|
||||
// Mock 데이터 (실제로는 API에서 가져옴)
|
||||
const post = useMemo(() => generateMockPost(postId), [postId]);
|
||||
|
||||
return <BoardForm mode="edit" initialData={post} />;
|
||||
}
|
||||
94
src/app/[locale]/(protected)/board/[id]/page.tsx
Normal file
94
src/app/[locale]/(protected)/board/[id]/page.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 게시글 상세 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { BoardDetail } from '@/components/board/BoardDetail';
|
||||
import type { Post, Comment } from '@/components/board/types';
|
||||
import { MOCK_BOARDS } from '@/components/board/types';
|
||||
|
||||
// 현재 로그인 사용자 ID (실제로는 auth context에서 가져옴)
|
||||
const CURRENT_USER_ID = 'user1';
|
||||
|
||||
// Mock 데이터 생성 (실제로는 API에서 가져옴)
|
||||
const generateMockPost = (id: string): Post => {
|
||||
const boards = MOCK_BOARDS.filter((b) => b.id !== 'all');
|
||||
const board = boards[0];
|
||||
|
||||
return {
|
||||
id,
|
||||
boardId: board.id,
|
||||
boardName: board.name,
|
||||
title: '제목',
|
||||
content: `
|
||||
<p>게시글 내용입니다.</p>
|
||||
<p>이것은 <strong>테스트용</strong> 콘텐츠입니다.</p>
|
||||
<p><img src="https://via.placeholder.com/600x300" alt="IMG" /></p>
|
||||
<p>내용</p>
|
||||
`,
|
||||
authorId: 'user1',
|
||||
authorName: '홍길동',
|
||||
authorDepartment: '개발팀',
|
||||
authorPosition: '과장',
|
||||
isPinned: false,
|
||||
allowComments: true,
|
||||
viewCount: 123,
|
||||
attachments: [
|
||||
{
|
||||
id: 'file-1',
|
||||
fileName: 'abc.pdf',
|
||||
fileSize: 1024000,
|
||||
fileUrl: '/files/abc.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
},
|
||||
],
|
||||
createdAt: format(new Date(2025, 8, 3, 12, 23), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 3, 12, 23), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
};
|
||||
};
|
||||
|
||||
const generateMockComments = (postId: string): Comment[] => [
|
||||
{
|
||||
id: 'comment-1',
|
||||
postId,
|
||||
authorId: 'user2',
|
||||
authorName: '이름 직책',
|
||||
authorDepartment: '부서명',
|
||||
authorPosition: '',
|
||||
content: '댓글 내용',
|
||||
createdAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
id: 'comment-2',
|
||||
postId,
|
||||
authorId: 'user1', // 본인 댓글
|
||||
authorName: '이름 직책',
|
||||
authorDepartment: '부서명',
|
||||
authorPosition: '',
|
||||
content: '댓글 내용',
|
||||
createdAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
},
|
||||
];
|
||||
|
||||
export default function BoardDetailPage() {
|
||||
const params = useParams();
|
||||
const postId = params.id as string;
|
||||
|
||||
// Mock 데이터 (실제로는 API에서 가져옴)
|
||||
const post = useMemo(() => generateMockPost(postId), [postId]);
|
||||
const comments = useMemo(() => generateMockComments(postId), [postId]);
|
||||
|
||||
return (
|
||||
<BoardDetail
|
||||
post={post}
|
||||
comments={comments}
|
||||
currentUserId={CURRENT_USER_ID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
|
||||
import type { Board, BoardFormData } from '@/components/board/BoardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockBoard: Board = {
|
||||
id: '1',
|
||||
target: 'all',
|
||||
boardName: '공지사항',
|
||||
status: 'active',
|
||||
authorId: 'u1',
|
||||
authorName: '홍길동',
|
||||
createdAt: '2025-09-09T12:20:00Z',
|
||||
updatedAt: '2025-09-09T12:20:00Z',
|
||||
};
|
||||
|
||||
export default function BoardEditPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setBoard(mockBoard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleSubmit = (data: BoardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Update board:', params.id, data);
|
||||
router.push(`/ko/board/board-management/${params.id}`);
|
||||
};
|
||||
|
||||
if (!board) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BoardForm
|
||||
mode="edit"
|
||||
board={board}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BoardDetail } from '@/components/board/BoardManagement/BoardDetail';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { Board } from '@/components/board/BoardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockBoard: Board = {
|
||||
id: '1',
|
||||
target: 'all',
|
||||
boardName: '공지사항',
|
||||
status: 'active',
|
||||
authorId: 'u1',
|
||||
authorName: '홍길동',
|
||||
createdAt: '2025-09-09T12:20:00Z',
|
||||
updatedAt: '2025-09-09T12:20:00Z',
|
||||
};
|
||||
|
||||
export default function BoardDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setBoard(mockBoard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/board/board-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// TODO: API 연동
|
||||
console.log('Delete board:', params.id);
|
||||
router.push('/ko/board/board-management');
|
||||
};
|
||||
|
||||
if (!board) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BoardDetail
|
||||
board={board}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시판 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{board.boardName}" 게시판을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 게시판 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
|
||||
import type { BoardFormData } from '@/components/board/BoardManagement/types';
|
||||
|
||||
export default function BoardNewPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = (data: BoardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Create board:', data);
|
||||
router.push('/ko/board/board-management');
|
||||
};
|
||||
|
||||
return (
|
||||
<BoardForm
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BoardManagement } from '@/components/board/BoardManagement';
|
||||
|
||||
export default function BoardManagementPage() {
|
||||
return <BoardManagement />;
|
||||
}
|
||||
14
src/app/[locale]/(protected)/board/create/page.tsx
Normal file
14
src/app/[locale]/(protected)/board/create/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 게시글 등록 페이지
|
||||
*/
|
||||
|
||||
import { BoardForm } from '@/components/board/BoardForm';
|
||||
|
||||
export default function BoardCreatePage() {
|
||||
return <BoardForm mode="create" />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '게시글 등록',
|
||||
description: '게시글을 등록하고 관리합니다.',
|
||||
};
|
||||
14
src/app/[locale]/(protected)/board/page.tsx
Normal file
14
src/app/[locale]/(protected)/board/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 게시판 목록 페이지
|
||||
*/
|
||||
|
||||
import { BoardList } from '@/components/board/BoardList';
|
||||
|
||||
export default function BoardPage() {
|
||||
return <BoardList />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '게시판',
|
||||
description: '게시판의 게시글을 등록하고 관리합니다.',
|
||||
};
|
||||
5
src/app/[locale]/(protected)/company-info/page.tsx
Normal file
5
src/app/[locale]/(protected)/company-info/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CompanyInfoManagement } from '@/components/settings/CompanyInfoManagement';
|
||||
|
||||
export default function CompanyInfoPage() {
|
||||
return <CompanyInfoManagement />;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { EventDetail, MOCK_EVENTS } from '@/components/customer-center/EventManagement';
|
||||
|
||||
export default function EventDetailPage() {
|
||||
const params = useParams();
|
||||
const eventId = params.id as string;
|
||||
|
||||
// Mock 데이터에서 이벤트 찾기
|
||||
const event = MOCK_EVENTS.find((e) => e.id === eventId);
|
||||
|
||||
if (!event) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-muted-foreground">이벤트를 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <EventDetail event={event} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { EventList } from '@/components/customer-center/EventManagement';
|
||||
|
||||
export default function EventsPage() {
|
||||
return <EventList />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { FAQList } from '@/components/customer-center/FAQManagement';
|
||||
|
||||
export default function FAQPage() {
|
||||
return <FAQList />;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 1:1 문의 수정 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { InquiryForm } from '@/components/customer-center/InquiryManagement';
|
||||
import { MOCK_INQUIRIES } from '@/components/customer-center/InquiryManagement/types';
|
||||
|
||||
export default function InquiryEditPage() {
|
||||
const params = useParams();
|
||||
const inquiryId = params.id as string;
|
||||
|
||||
// Mock: 문의 데이터 조회
|
||||
const inquiry = MOCK_INQUIRIES.find((i) => i.id === inquiryId);
|
||||
|
||||
if (!inquiry) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">문의를 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <InquiryForm mode="edit" initialData={inquiry} />;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 1:1 문의 상세 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { InquiryDetail } from '@/components/customer-center/InquiryManagement';
|
||||
import { MOCK_INQUIRIES, MOCK_REPLY, MOCK_COMMENTS } from '@/components/customer-center/InquiryManagement/types';
|
||||
|
||||
export default function InquiryDetailPage() {
|
||||
const params = useParams();
|
||||
const inquiryId = params.id as string;
|
||||
|
||||
// Mock: 문의 데이터 조회
|
||||
const inquiry = MOCK_INQUIRIES.find((i) => i.id === inquiryId);
|
||||
|
||||
if (!inquiry) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">문의를 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mock: 답변 데이터 (답변완료 상태일 때만)
|
||||
const reply = inquiry.status === 'completed' ? MOCK_REPLY : undefined;
|
||||
|
||||
// Mock: 댓글 데이터
|
||||
const comments = MOCK_COMMENTS.filter((c) => c.inquiryId === inquiryId);
|
||||
|
||||
// Mock: 현재 사용자 ID
|
||||
const currentUserId = 'user1';
|
||||
|
||||
return (
|
||||
<InquiryDetail
|
||||
inquiry={inquiry}
|
||||
reply={reply}
|
||||
comments={comments}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 1:1 문의 등록 페이지
|
||||
*/
|
||||
|
||||
import { InquiryForm } from '@/components/customer-center/InquiryManagement';
|
||||
|
||||
export default function InquiryCreatePage() {
|
||||
return <InquiryForm mode="create" />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '1:1 문의 등록',
|
||||
description: '1:1 문의를 등록합니다.',
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 1:1 문의 목록 페이지
|
||||
*/
|
||||
|
||||
import { InquiryList } from '@/components/customer-center/InquiryManagement';
|
||||
|
||||
export default function InquiriesPage() {
|
||||
return <InquiryList />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '1:1 문의',
|
||||
description: '1:1 문의를 등록하고 답변을 확인합니다.',
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { NoticeDetail, MOCK_NOTICES } from '@/components/customer-center/NoticeManagement';
|
||||
|
||||
export default function NoticeDetailPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
// Mock 데이터에서 해당 공지사항 찾기
|
||||
const notice = MOCK_NOTICES.find((n) => n.id === id);
|
||||
|
||||
if (!notice) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-muted-foreground">공지사항을 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <NoticeDetail notice={notice} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { NoticeList } from '@/components/customer-center/NoticeManagement';
|
||||
|
||||
export default function NoticesPage() {
|
||||
return <NoticeList />;
|
||||
}
|
||||
267
src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx
Normal file
267
src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ExternalLink, Copy, Check, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react';
|
||||
|
||||
export interface UrlItem {
|
||||
name: string;
|
||||
url: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface UrlCategory {
|
||||
title: string;
|
||||
icon: string;
|
||||
items: UrlItem[];
|
||||
subCategories?: {
|
||||
title: string;
|
||||
items: UrlItem[];
|
||||
}[];
|
||||
}
|
||||
|
||||
interface TestUrlsClientProps {
|
||||
initialData: UrlCategory[];
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
function UrlCard({ item, baseUrl }: { item: UrlItem; baseUrl: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const fullUrl = `${baseUrl}${item.url}`;
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
await navigator.clipboard.writeText(fullUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
window.open(fullUrl, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleOpen}
|
||||
className="group flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-500 hover:shadow-md transition-all cursor-pointer"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900 dark:text-white truncate">
|
||||
{item.name}
|
||||
</span>
|
||||
{item.status && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700">
|
||||
{item.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 truncate mt-0.5">
|
||||
{item.url}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
title="URL 복사"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategorySection({ category, baseUrl }: { category: UrlCategory; baseUrl: string }) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [subExpanded, setSubExpanded] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleSub = (title: string) => {
|
||||
setSubExpanded((prev) => ({ ...prev, [title]: !prev[title] }));
|
||||
};
|
||||
|
||||
const totalItems = category.items.length +
|
||||
(category.subCategories?.reduce((acc, sub) => acc + sub.items.length, 0) || 0);
|
||||
|
||||
if (totalItems === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 w-full text-left mb-3"
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="w-5 h-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-gray-500" />
|
||||
)}
|
||||
<span className="text-xl">{category.icon}</span>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{category.title}
|
||||
</h2>
|
||||
<span className="text-sm text-gray-500 ml-auto">
|
||||
{totalItems} 링크
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="pl-7 space-y-4">
|
||||
{category.items.length > 0 && (
|
||||
<div className="grid gap-2">
|
||||
{category.items.map((item) => (
|
||||
<UrlCard key={item.url} item={item} baseUrl={baseUrl} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{category.subCategories?.map((sub) => (
|
||||
<div key={sub.title} className="mt-3">
|
||||
<button
|
||||
onClick={() => toggleSub(sub.title)}
|
||||
className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-2 hover:text-gray-900 dark:hover:text-white"
|
||||
>
|
||||
{subExpanded[sub.title] !== false ? (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
)}
|
||||
{sub.title}
|
||||
<span className="text-xs text-gray-400">({sub.items.length})</span>
|
||||
</button>
|
||||
{subExpanded[sub.title] !== false && (
|
||||
<div className="grid gap-2 pl-6">
|
||||
{sub.items.map((item) => (
|
||||
<UrlCard key={item.url} item={item} baseUrl={baseUrl} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TestUrlsClient({ initialData, lastUpdated }: TestUrlsClientProps) {
|
||||
const [baseUrl, setBaseUrl] = useState('http://localhost:3000');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setBaseUrl(window.location.origin);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 검색 필터링
|
||||
const filteredData = initialData
|
||||
.map((category) => ({
|
||||
...category,
|
||||
items: category.items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.url.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
),
|
||||
subCategories: category.subCategories?.map((sub) => ({
|
||||
...sub,
|
||||
items: sub.items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.url.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
),
|
||||
})).filter((sub) => sub.items.length > 0),
|
||||
}))
|
||||
.filter(
|
||||
(category) =>
|
||||
category.items.length > 0 || (category.subCategories && category.subCategories.length > 0)
|
||||
);
|
||||
|
||||
const totalLinks = initialData.reduce(
|
||||
(acc, cat) =>
|
||||
acc +
|
||||
cat.items.length +
|
||||
(cat.subCategories?.reduce((subAcc, sub) => subAcc + sub.items.length, 0) || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const handleRefresh = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
🔗 테스트 URL 목록
|
||||
</h1>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title="새로고침 (md 파일 변경사항 반영)"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
새로고침
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
백엔드 메뉴 연동 전 테스트용 직접 접근 URL ({totalLinks}개)
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
클릭하면 새 탭에서 열립니다 • 최종 업데이트: {lastUpdated}
|
||||
</p>
|
||||
<p className="text-xs text-green-600 dark:text-green-400 mt-1">
|
||||
✨ md 파일 수정 시 자동 반영됩니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search & Base URL */}
|
||||
<div className="flex gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="페이지 또는 URL 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-300 dark:border-gray-600">
|
||||
<span className="text-sm text-gray-500">Base:</span>
|
||||
<input
|
||||
type="text"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
className="w-48 text-sm bg-transparent text-gray-900 dark:text-white focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="space-y-2">
|
||||
{filteredData.map((category) => (
|
||||
<CategorySection key={category.title} category={category} baseUrl={baseUrl} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredData.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 text-center text-sm text-gray-500">
|
||||
<p>
|
||||
📁 데이터 소스: <code className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">claudedocs/[REF] all-pages-test-urls.md</code>
|
||||
</p>
|
||||
<p className="mt-1 text-green-600 dark:text-green-400">
|
||||
md 파일 수정 후 새로고침하면 자동 반영!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
src/app/[locale]/(protected)/dev/test-urls/page.tsx
Normal file
167
src/app/[locale]/(protected)/dev/test-urls/page.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import TestUrlsClient, { UrlCategory, UrlItem } from './TestUrlsClient';
|
||||
|
||||
// 아이콘 매핑
|
||||
const iconMap: Record<string, string> = {
|
||||
'기본': '🏠',
|
||||
'인사관리': '👥',
|
||||
'HR': '👥',
|
||||
'판매관리': '💰',
|
||||
'Sales': '💰',
|
||||
'기준정보관리': '📦',
|
||||
'Master Data': '📦',
|
||||
'생산관리': '🏭',
|
||||
'Production': '🏭',
|
||||
'설정': '⚙️',
|
||||
'Settings': '⚙️',
|
||||
'전자결재': '📝',
|
||||
'Approval': '📝',
|
||||
'회계관리': '💵',
|
||||
'Accounting': '💵',
|
||||
'게시판': '📋',
|
||||
'Board': '📋',
|
||||
'보고서': '📊',
|
||||
'Reports': '📊',
|
||||
};
|
||||
|
||||
function getIcon(title: string): string {
|
||||
for (const [key, icon] of Object.entries(iconMap)) {
|
||||
if (title.includes(key)) return icon;
|
||||
}
|
||||
return '📄';
|
||||
}
|
||||
|
||||
function parseTableRow(line: string): UrlItem | null {
|
||||
// | 페이지 | URL | 상태 | 형식 파싱
|
||||
const parts = line.split('|').map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (parts.length < 2) return null;
|
||||
if (parts[0] === '페이지' || parts[0].startsWith('---')) return null;
|
||||
|
||||
const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거
|
||||
const url = parts[1].replace(/`/g, ''); // backtick 제거
|
||||
const status = parts[2] || undefined;
|
||||
|
||||
// URL이 /ko로 시작하는지 확인
|
||||
if (!url.startsWith('/ko')) return null;
|
||||
|
||||
return { name, url, status };
|
||||
}
|
||||
|
||||
function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: string } {
|
||||
const lines = content.split('\n');
|
||||
const categories: UrlCategory[] = [];
|
||||
let currentCategory: UrlCategory | null = null;
|
||||
let currentSubCategory: { title: string; items: UrlItem[] } | null = null;
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
// Last Updated 추출
|
||||
const updateMatch = content.match(/Last Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
||||
if (updateMatch) {
|
||||
lastUpdated = updateMatch[1];
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// ## 카테고리 (메인 섹션)
|
||||
if (line.startsWith('## ') && !line.includes('클릭 가능한') && !line.includes('전체 URL') && !line.includes('백엔드 메뉴')) {
|
||||
// 이전 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
currentSubCategory = null;
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim();
|
||||
currentCategory = {
|
||||
title,
|
||||
icon: getIcon(title),
|
||||
items: [],
|
||||
subCategories: [],
|
||||
};
|
||||
currentSubCategory = null;
|
||||
}
|
||||
|
||||
// ### 서브 카테고리
|
||||
else if (line.startsWith('### ') && currentCategory) {
|
||||
// 이전 서브카테고리 저장
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
|
||||
const subTitle = line.replace('### ', '').trim();
|
||||
// "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로
|
||||
if (subTitle === '메인 페이지') {
|
||||
currentSubCategory = null;
|
||||
} else {
|
||||
currentSubCategory = {
|
||||
title: subTitle,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 행 파싱
|
||||
else if (line.startsWith('|') && currentCategory) {
|
||||
const item = parseTableRow(line);
|
||||
if (item) {
|
||||
if (currentSubCategory) {
|
||||
currentSubCategory.items.push(item);
|
||||
} else {
|
||||
currentCategory.items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
// 빈 서브카테고리 제거
|
||||
categories.forEach(cat => {
|
||||
cat.subCategories = cat.subCategories?.filter(sub => sub.items.length > 0);
|
||||
});
|
||||
|
||||
return { categories, lastUpdated };
|
||||
}
|
||||
|
||||
export default async function TestUrlsPage() {
|
||||
// md 파일 경로
|
||||
const mdFilePath = path.join(
|
||||
process.cwd(),
|
||||
'claudedocs',
|
||||
'[REF] all-pages-test-urls.md'
|
||||
);
|
||||
|
||||
let urlData: UrlCategory[] = [];
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(mdFilePath, 'utf-8');
|
||||
const parsed = parseMdFile(fileContent);
|
||||
urlData = parsed.categories;
|
||||
lastUpdated = parsed.lastUpdated;
|
||||
} catch (error) {
|
||||
console.error('Failed to read md file:', error);
|
||||
// 파일 읽기 실패 시 빈 데이터
|
||||
urlData = [];
|
||||
}
|
||||
|
||||
return <TestUrlsClient initialData={urlData} lastUpdated={lastUpdated} />;
|
||||
}
|
||||
|
||||
// 캐싱 비활성화 - 항상 최신 md 파일 읽기
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 0;
|
||||
295
src/app/[locale]/(protected)/hr/attendance/page.tsx
Normal file
295
src/app/[locale]/(protected)/hr/attendance/page.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { MapPin } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import GoogleMap from '@/components/attendance/GoogleMap';
|
||||
import AttendanceComplete from '@/components/attendance/AttendanceComplete';
|
||||
|
||||
// ========================================
|
||||
// 하드코딩 설정값 (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 값으로 대체 예정
|
||||
};
|
||||
|
||||
const TEST_USER = {
|
||||
name: '홍길동',
|
||||
department: '부서명',
|
||||
position: '직급명',
|
||||
};
|
||||
|
||||
// 출퇴근 상태 타입
|
||||
type AttendanceStatus = 'not-checked-in' | 'checked-in' | 'checked-out';
|
||||
type ViewMode = 'main' | 'check-in-complete' | 'check-out-complete';
|
||||
|
||||
export default function MobileAttendancePage() {
|
||||
// Hydration 에러 방지: 클라이언트 마운트 상태
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// 상태 관리
|
||||
const [currentTime, setCurrentTime] = useState<string>('--:--:--');
|
||||
const [currentDate, setCurrentDate] = useState<string>('');
|
||||
const [distance, setDistance] = useState<number | null>(null);
|
||||
const [isInRange, setIsInRange] = useState(false);
|
||||
const [attendanceStatus, setAttendanceStatus] = useState<AttendanceStatus>('not-checked-in');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('main');
|
||||
const [checkInTime, setCheckInTime] = useState<string>('');
|
||||
const [checkOutTime, setCheckOutTime] = useState<string>('');
|
||||
const [userName, setUserName] = useState(TEST_USER.name);
|
||||
const [userDepartment, setUserDepartment] = useState(TEST_USER.department);
|
||||
const [userPosition, setUserPosition] = useState(TEST_USER.position);
|
||||
|
||||
// 클라이언트 마운트 확인
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// 현재 시간 업데이트 (마운트 후에만 실행)
|
||||
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();
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
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 {
|
||||
const userData = JSON.parse(userDataStr);
|
||||
if (userData.name) setUserName(userData.name);
|
||||
if (userData.department) setUserDepartment(userData.department);
|
||||
if (userData.position) setUserPosition(userData.position);
|
||||
} catch (e) {
|
||||
console.error('사용자 정보 파싱 실패:', e);
|
||||
}
|
||||
}
|
||||
}, [mounted]);
|
||||
|
||||
// 거리 변경 콜백
|
||||
const handleDistanceChange = useCallback((dist: number, inRange: boolean) => {
|
||||
setDistance(dist);
|
||||
setIsInRange(inRange);
|
||||
}, []);
|
||||
|
||||
// 출근하기
|
||||
const handleCheckIn = () => {
|
||||
if (!isInRange) return;
|
||||
|
||||
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}`;
|
||||
|
||||
setCheckInTime(timeStr);
|
||||
setAttendanceStatus('checked-in');
|
||||
setViewMode('check-in-complete');
|
||||
|
||||
// TODO: API 호출로 출근 기록 저장
|
||||
console.log('[출근 기록]', {
|
||||
time: timeStr,
|
||||
location: SITE_LOCATION.name,
|
||||
coordinates: { lat: SITE_LOCATION.lat, lng: SITE_LOCATION.lng },
|
||||
});
|
||||
};
|
||||
|
||||
// 퇴근하기
|
||||
const handleCheckOut = () => {
|
||||
if (!isInRange) return;
|
||||
|
||||
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}`;
|
||||
|
||||
setCheckOutTime(timeStr);
|
||||
setAttendanceStatus('checked-out');
|
||||
setViewMode('check-out-complete');
|
||||
|
||||
// TODO: API 호출로 퇴근 기록 저장
|
||||
console.log('[퇴근 기록]', {
|
||||
time: timeStr,
|
||||
location: SITE_LOCATION.name,
|
||||
coordinates: { lat: SITE_LOCATION.lat, lng: SITE_LOCATION.lng },
|
||||
});
|
||||
};
|
||||
|
||||
// 완료 화면에서 확인 클릭
|
||||
const handleConfirm = () => {
|
||||
setViewMode('main');
|
||||
};
|
||||
|
||||
// 마운트 전 로딩 UI (Hydration 에러 방지)
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-100px)]">
|
||||
<div className="text-center py-3 border-b bg-white">
|
||||
<h1 className="text-lg font-semibold">출퇴근하기</h1>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-gray-500 text-sm">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 완료 화면 렌더링
|
||||
if (viewMode === 'check-in-complete') {
|
||||
return (
|
||||
<div className="h-[calc(100vh-100px)]">
|
||||
<AttendanceComplete
|
||||
type="check-in"
|
||||
time={checkInTime}
|
||||
date={currentDate}
|
||||
location={SITE_LOCATION.name}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (viewMode === 'check-out-complete') {
|
||||
return (
|
||||
<div className="h-[calc(100vh-100px)]">
|
||||
<AttendanceComplete
|
||||
type="check-out"
|
||||
time={checkOutTime}
|
||||
date={currentDate}
|
||||
location={SITE_LOCATION.name}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 버튼 활성화 상태
|
||||
const canCheckIn = isInRange && attendanceStatus === 'not-checked-in';
|
||||
const canCheckOut = isInRange && attendanceStatus === 'checked-in';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-100px)]">
|
||||
{/* 타이틀 */}
|
||||
<div className="text-center py-3 border-b bg-white">
|
||||
<h1 className="text-lg font-semibold">출퇴근하기</h1>
|
||||
</div>
|
||||
|
||||
{/* 지도 영역 */}
|
||||
<div className="flex-1 relative">
|
||||
<GoogleMap
|
||||
siteLocation={SITE_LOCATION}
|
||||
onDistanceChange={handleDistanceChange}
|
||||
/>
|
||||
|
||||
{/* 거리 표시 오버레이 */}
|
||||
{distance !== null && (
|
||||
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur px-3 py-1.5 rounded-lg shadow-md">
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<MapPin className="w-4 h-4 text-blue-500" />
|
||||
<span className={isInRange ? 'text-green-600 font-medium' : 'text-gray-600'}>
|
||||
{distance < 1000
|
||||
? `${Math.round(distance)}m`
|
||||
: `${(distance / 1000).toFixed(1)}km`}
|
||||
{isInRange && ' (범위 내)'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 사용자 정보 + 시간 + 버튼 */}
|
||||
<div className="bg-white border-t p-4 space-y-4">
|
||||
{/* 사용자 정보 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center">
|
||||
<span className="text-gray-600 font-medium">{userName.charAt(0)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">{userName}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{userDepartment} {userPosition}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 현재 시간 */}
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-red-500" suppressHydrationWarning>{currentTime}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5" suppressHydrationWarning>{currentDate}</p>
|
||||
{attendanceStatus === 'checked-in' && (
|
||||
<p className="text-sm text-green-600 mt-1">출근중</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 출근/퇴근 버튼 */}
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleCheckIn}
|
||||
disabled={!canCheckIn}
|
||||
className={`flex-1 h-12 text-base font-medium rounded-lg transition-all ${
|
||||
canCheckIn
|
||||
? 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
출근하기
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCheckOut}
|
||||
disabled={!canCheckOut}
|
||||
className={`flex-1 h-12 text-base font-medium rounded-lg transition-all ${
|
||||
canCheckOut
|
||||
? 'bg-gray-800 hover:bg-gray-900 text-white'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
퇴근하기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 범위 밖 경고 */}
|
||||
{!isInRange && distance !== null && (
|
||||
<p className="text-center text-sm text-orange-500">
|
||||
출퇴근 가능 범위({SITE_LOCATION.radius}m) 밖에 있습니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CardForm } from '@/components/hr/CardManagement/CardForm';
|
||||
import type { Card, CardFormData } from '@/components/hr/CardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockCard: Card = {
|
||||
id: '1',
|
||||
cardCompany: 'shinhan',
|
||||
cardNumber: '1234-1234-1234-1234',
|
||||
cardName: '법인카드1',
|
||||
expiryDate: '0327',
|
||||
pinPrefix: '12',
|
||||
status: 'active',
|
||||
user: {
|
||||
id: 'u1',
|
||||
departmentId: 'd1',
|
||||
departmentName: '부서명',
|
||||
employeeId: 'e1',
|
||||
employeeName: '홍길동',
|
||||
positionId: 'p1',
|
||||
positionName: '팀장',
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
export default function CardEditPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [card, setCard] = useState<Card | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setCard(mockCard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleSubmit = (data: CardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Update card:', params.id, data);
|
||||
router.push(`/ko/hr/card-management/${params.id}`);
|
||||
};
|
||||
|
||||
if (!card) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardForm
|
||||
mode="edit"
|
||||
card={card}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
110
src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx
Normal file
110
src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CardDetail } from '@/components/hr/CardManagement/CardDetail';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { Card } from '@/components/hr/CardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockCard: Card = {
|
||||
id: '1',
|
||||
cardCompany: 'shinhan',
|
||||
cardNumber: '1234-1234-1234-1234',
|
||||
cardName: '법인카드1',
|
||||
expiryDate: '0327',
|
||||
pinPrefix: '12',
|
||||
status: 'active',
|
||||
user: {
|
||||
id: 'u1',
|
||||
departmentId: 'd1',
|
||||
departmentName: '부서명',
|
||||
employeeId: 'e1',
|
||||
employeeName: '홍길동',
|
||||
positionId: 'p1',
|
||||
positionName: '팀장',
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
export default function CardDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [card, setCard] = useState<Card | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setCard(mockCard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/hr/card-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// TODO: API 연동
|
||||
console.log('Delete card:', params.id);
|
||||
router.push('/ko/hr/card-management');
|
||||
};
|
||||
|
||||
if (!card) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardDetail
|
||||
card={card}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>카드 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{card.cardName}" 카드를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 카드 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
src/app/[locale]/(protected)/hr/card-management/new/page.tsx
Normal file
22
src/app/[locale]/(protected)/hr/card-management/new/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { CardForm } from '@/components/hr/CardManagement/CardForm';
|
||||
import type { CardFormData } from '@/components/hr/CardManagement/types';
|
||||
|
||||
export default function CardNewPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = (data: CardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Create card:', data);
|
||||
router.push('/ko/hr/card-management');
|
||||
};
|
||||
|
||||
return (
|
||||
<CardForm
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
7
src/app/[locale]/(protected)/hr/card-management/page.tsx
Normal file
7
src/app/[locale]/(protected)/hr/card-management/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { CardManagement } from '@/components/hr/CardManagement';
|
||||
|
||||
export default function CardManagementPage() {
|
||||
return <CardManagement />;
|
||||
}
|
||||
@@ -31,6 +31,10 @@ const mockEmployee: Employee = {
|
||||
departmentPositions: [
|
||||
{ id: '1', departmentId: 'd1', departmentName: '개발본부', positionId: 'p1', positionName: '팀장' }
|
||||
],
|
||||
clockInLocation: 'headquarters',
|
||||
clockOutLocation: 'headquarters',
|
||||
concurrentPosition: '',
|
||||
concurrentReason: '',
|
||||
userInfo: { userId: 'kimcs', role: 'manager', accountStatus: 'active' },
|
||||
createdAt: '2020-03-15T00:00:00Z',
|
||||
updatedAt: '2024-01-15T00:00:00Z',
|
||||
|
||||
@@ -41,6 +41,10 @@ const mockEmployee: Employee = {
|
||||
departmentPositions: [
|
||||
{ id: '1', departmentId: 'd1', departmentName: '개발본부', positionId: 'p1', positionName: '팀장' }
|
||||
],
|
||||
clockInLocation: 'headquarters',
|
||||
clockOutLocation: 'headquarters',
|
||||
concurrentPosition: '',
|
||||
concurrentReason: '',
|
||||
userInfo: { userId: 'kimcs', role: 'manager', accountStatus: 'active' },
|
||||
createdAt: '2020-03-15T00:00:00Z',
|
||||
updatedAt: '2024-01-15T00:00:00Z',
|
||||
|
||||
@@ -430,7 +430,7 @@ export default function EditItemPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<DynamicItemForm
|
||||
mode="edit"
|
||||
itemType={itemType}
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function ItemDetailPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<ItemDetailClient item={item} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function CreateItemPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
{submitError && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-900">
|
||||
⚠️ {submitError}
|
||||
|
||||
@@ -12,11 +12,7 @@ import ItemListClient from '@/components/items/ItemListClient';
|
||||
* 품목 목록 페이지
|
||||
*/
|
||||
export default function ItemsPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ItemListClient />
|
||||
</div>
|
||||
);
|
||||
return <ItemListClient />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
src/app/[locale]/(protected)/payment-history/page.tsx
Normal file
5
src/app/[locale]/(protected)/payment-history/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PaymentHistoryManagement } from '@/components/settings/PaymentHistoryManagement';
|
||||
|
||||
export default function PaymentHistoryPage() {
|
||||
return <PaymentHistoryManagement />;
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export default function EditItemPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<div className="text-center py-8">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
@@ -201,7 +201,7 @@ export default function EditItemPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<ItemForm mode="edit" initialData={item} onSubmit={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -163,7 +163,7 @@ export default async function ItemDetailPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
|
||||
<ItemDetailClient item={item} />
|
||||
</Suspense>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function CreateItemPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<ItemForm mode="create" onSubmit={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -145,11 +145,9 @@ export default async function ItemsPage() {
|
||||
const items = await getItems();
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
|
||||
<ItemListClient />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
|
||||
<ItemListClient />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import ComprehensiveAnalysis from '@/components/reports/ComprehensiveAnalysis';
|
||||
|
||||
export default function ComprehensiveAnalysisPage() {
|
||||
return <ComprehensiveAnalysis />;
|
||||
}
|
||||
6
src/app/[locale]/(protected)/reports/page.tsx
Normal file
6
src/app/[locale]/(protected)/reports/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function ReportsPage() {
|
||||
// 보고서 및 분석 메인 → 종합 경영 분석으로 리다이렉트
|
||||
redirect('/ko/reports/comprehensive-analysis');
|
||||
}
|
||||
@@ -140,6 +140,12 @@ function mapStatus(apiStatus: string, isFinal: boolean): PricingStatus | 'not_re
|
||||
}
|
||||
}
|
||||
|
||||
// 품목 유형 → API item_type_code 매핑 (백엔드 pricing API용)
|
||||
function mapItemTypeCode(itemType?: string): 'PRODUCT' | 'MATERIAL' {
|
||||
// FG(제품)만 PRODUCT, 나머지는 모두 MATERIAL
|
||||
return itemType === 'FG' ? 'PRODUCT' : 'MATERIAL';
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 호출 함수
|
||||
// ============================================
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AccountInfoClient } from '@/components/settings/AccountInfoManagement';
|
||||
|
||||
export default function AccountInfoPage() {
|
||||
return <AccountInfoClient />;
|
||||
}
|
||||
129
src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx
Normal file
129
src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AccountDetail } from '@/components/settings/AccountManagement/AccountDetail';
|
||||
import type { Account } from '@/components/settings/AccountManagement/types';
|
||||
|
||||
// Mock 데이터 (API 연동 전 임시)
|
||||
const mockAccounts: Account[] = [
|
||||
{
|
||||
id: 'account-1',
|
||||
bankCode: 'shinhan',
|
||||
accountNumber: '1234-1234-1234-1234',
|
||||
accountName: '운영계좌 1',
|
||||
accountHolder: '예금주1',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
bankCode: 'kb',
|
||||
accountNumber: '1234-1234-1234-1235',
|
||||
accountName: '운영계좌 2',
|
||||
accountHolder: '예금주2',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-3',
|
||||
bankCode: 'woori',
|
||||
accountNumber: '1234-1234-1234-1236',
|
||||
accountName: '운영계좌 3',
|
||||
accountHolder: '예금주3',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-4',
|
||||
bankCode: 'hana',
|
||||
accountNumber: '1234-1234-1234-1237',
|
||||
accountName: '운영계좌 4',
|
||||
accountHolder: '예금주4',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-5',
|
||||
bankCode: 'nh',
|
||||
accountNumber: '1234-1234-1234-1238',
|
||||
accountName: '운영계좌 5',
|
||||
accountHolder: '예금주5',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-6',
|
||||
bankCode: 'ibk',
|
||||
accountNumber: '1234-1234-1234-1239',
|
||||
accountName: '운영계좌 6',
|
||||
accountHolder: '예금주6',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-7',
|
||||
bankCode: 'shinhan',
|
||||
accountNumber: '1234-1234-1234-1240',
|
||||
accountName: '운영계좌 7',
|
||||
accountHolder: '예금주7',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-8',
|
||||
bankCode: 'kb',
|
||||
accountNumber: '1234-1234-1234-1241',
|
||||
accountName: '운영계좌 8',
|
||||
accountHolder: '예금주8',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-9',
|
||||
bankCode: 'woori',
|
||||
accountNumber: '1234-1234-1234-1242',
|
||||
accountName: '운영계좌 9',
|
||||
accountHolder: '예금주9',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-10',
|
||||
bankCode: 'hana',
|
||||
accountNumber: '1234-1234-1234-1243',
|
||||
accountName: '운영계좌 10',
|
||||
accountHolder: '예금주10',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
export default function AccountDetailPage() {
|
||||
const params = useParams();
|
||||
const accountId = params.id as string;
|
||||
|
||||
// Mock: 계좌 조회
|
||||
const account = mockAccounts.find(a => a.id === accountId);
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
계좌를 찾을 수 없습니다.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <AccountDetail account={account} mode="view" />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { AccountDetail } from '@/components/settings/AccountManagement/AccountDetail';
|
||||
|
||||
export default function NewAccountPage() {
|
||||
return <AccountDetail mode="create" />;
|
||||
}
|
||||
7
src/app/[locale]/(protected)/settings/accounts/page.tsx
Normal file
7
src/app/[locale]/(protected)/settings/accounts/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { AccountManagement } from '@/components/settings/AccountManagement';
|
||||
|
||||
export default function AccountsPage() {
|
||||
return <AccountManagement />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AttendanceSettingsManagement } from '@/components/settings/AttendanceSettingsManagement';
|
||||
|
||||
export default function AttendanceSettingsPage() {
|
||||
return <AttendanceSettingsManagement />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NotificationSettingsManagement } from '@/components/settings/NotificationSettings';
|
||||
|
||||
export default function NotificationSettingsPage() {
|
||||
return <NotificationSettingsManagement />;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PopupForm } from '@/components/settings/PopupManagement';
|
||||
import { MOCK_POPUPS } from '@/components/settings/PopupManagement/types';
|
||||
|
||||
export default function PopupEditPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
// Mock: ID로 팝업 데이터 조회
|
||||
const popup = MOCK_POPUPS.find((p) => p.id === id);
|
||||
|
||||
if (!popup) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">팝업을 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <PopupForm mode="edit" initialData={popup} />;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PopupDetail } from '@/components/settings/PopupManagement';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { MOCK_POPUPS, type Popup } from '@/components/settings/PopupManagement/types';
|
||||
|
||||
export default function PopupDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [popup, setPopup] = useState<Popup | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
const id = params.id as string;
|
||||
const found = MOCK_POPUPS.find((p) => p.id === id);
|
||||
setPopup(found || null);
|
||||
}, [params.id]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/settings/popup-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// TODO: API 연동
|
||||
console.log('Delete popup:', params.id);
|
||||
router.push('/ko/settings/popup-management');
|
||||
};
|
||||
|
||||
if (!popup) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopupDetail
|
||||
popup={popup}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>팝업 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{popup.title}" 팝업을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 팝업 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PopupForm } from '@/components/settings/PopupManagement';
|
||||
|
||||
export default function PopupCreatePage() {
|
||||
return <PopupForm mode="create" />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PopupList } from '@/components/settings/PopupManagement';
|
||||
|
||||
export default function PopupManagementPage() {
|
||||
return <PopupList />;
|
||||
}
|
||||
7
src/app/[locale]/(protected)/subscription/page.tsx
Normal file
7
src/app/[locale]/(protected)/subscription/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { SubscriptionManagement } from '@/components/settings/SubscriptionManagement';
|
||||
|
||||
export default function SubscriptionPage() {
|
||||
return <SubscriptionManagement />;
|
||||
}
|
||||
Reference in New Issue
Block a user