feat(WEB): 전체 페이지 ?mode= URL 네비게이션 패턴 적용
- 등록(?mode=new), 상세(?mode=view), 수정(?mode=edit) URL 패턴 일괄 적용
- 중복 패턴 제거: /edit?mode=edit → ?mode=edit (16개 파일)
- 제목 일관성: {기능} 등록/상세/수정 패턴 적용
- 검수 체크리스트 문서 추가 (79개 페이지)
- UniversalListPage, IntegratedDetailTemplate 공통 컴포넌트 개선
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 악성채권 추심관리 목록 페이지
|
||||
* 악성채권 추심관리 목록/등록 페이지
|
||||
*
|
||||
* 경로: /accounting/bad-debt-collection
|
||||
* API:
|
||||
* - GET /api/v1/bad-debts - 악성채권 목록
|
||||
* - GET /api/v1/bad-debts/summary - 통계 정보
|
||||
* 경로: /accounting/bad-debt-collection?mode=new
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BadDebtCollection } from '@/components/accounting/BadDebtCollection';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { BadDebtCollection, BadDebtDetailClientV2 } from '@/components/accounting/BadDebtCollection';
|
||||
import { getBadDebts, getBadDebtSummary } from '@/components/accounting/BadDebtCollection/actions';
|
||||
import type { BadDebtSummary } from '@/components/accounting/BadDebtCollection/types';
|
||||
|
||||
@@ -23,21 +22,32 @@ const DEFAULT_SUMMARY: BadDebtSummary = {
|
||||
};
|
||||
|
||||
export default function BadDebtCollectionPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getBadDebts>>>([]);
|
||||
const [summary, setSummary] = useState<BadDebtSummary>(DEFAULT_SUMMARY);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getBadDebts({ size: 100 }),
|
||||
getBadDebtSummary(),
|
||||
])
|
||||
.then(([badDebts, summaryResult]) => {
|
||||
setData(badDebts);
|
||||
setSummary(summaryResult);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
if (mode !== 'new') {
|
||||
Promise.all([
|
||||
getBadDebts({ size: 100 }),
|
||||
getBadDebtSummary(),
|
||||
])
|
||||
.then(([badDebts, summaryResult]) => {
|
||||
setData(badDebts);
|
||||
setSummary(summaryResult);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
if (mode === 'new') {
|
||||
return <BadDebtDetailClientV2 />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -53,4 +63,4 @@ export default function BadDebtCollectionPage() {
|
||||
initialSummary={summary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { BillManagementClient } from '@/components/accounting/BillManagement/BillManagementClient';
|
||||
import { BillDetail } from '@/components/accounting/BillManagement/BillDetail';
|
||||
import { getBills } from '@/components/accounting/BillManagement/actions';
|
||||
import type { BillRecord } from '@/components/accounting/BillManagement/types';
|
||||
|
||||
@@ -15,6 +16,7 @@ const DEFAULT_PAGINATION = {
|
||||
|
||||
export default function BillsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
const vendorId = searchParams.get('vendorId') || undefined;
|
||||
const billType = searchParams.get('type') || 'received';
|
||||
const page = searchParams.get('page') ? parseInt(searchParams.get('page')!) : 1;
|
||||
@@ -24,15 +26,23 @@ export default function BillsPage() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getBills({ billType, page, perPage: 20 })
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [billType, page]);
|
||||
if (mode !== 'new') {
|
||||
getBills({ billType, page, perPage: 20 })
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
}
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [mode, billType, page]);
|
||||
|
||||
if (mode === 'new') {
|
||||
return <BillDetail billId="new" mode="new" />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -50,4 +60,4 @@ export default function BillsPage() {
|
||||
initialBillType={billType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { CardTransactionInquiry } from '@/components/accounting/CardTransactionInquiry';
|
||||
import CardTransactionDetailClient from '@/components/accounting/CardTransactionInquiry/CardTransactionDetailClient';
|
||||
|
||||
export default function CardTransactionsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <CardTransactionDetailClient initialMode="create" />;
|
||||
}
|
||||
|
||||
return <CardTransactionInquiry />;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { VendorManagement } from '@/components/accounting/VendorManagement';
|
||||
import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail';
|
||||
import { getClients } from '@/components/accounting/VendorManagement/actions';
|
||||
|
||||
export default function VendorsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getClients>>['data']>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getClients({ size: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
if (mode !== 'new') {
|
||||
getClients({ size: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
if (mode === 'new') {
|
||||
return <VendorDetail mode="new" />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -32,4 +45,4 @@ export default function VendorsPage() {
|
||||
initialTotal={total}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { DraftBox } from '@/components/approval/DraftBox';
|
||||
import { DocumentCreate } from '@/components/approval/DocumentCreate';
|
||||
|
||||
export default function DraftBoxPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <DocumentCreate />;
|
||||
}
|
||||
|
||||
return <DraftBox />;
|
||||
}
|
||||
|
||||
435
src/app/[locale]/(protected)/board/[boardCode]/page.tsx
Normal file
435
src/app/[locale]/(protected)/board/[boardCode]/page.tsx
Normal file
@@ -0,0 +1,435 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 특정 게시판의 게시글 목록 페이지
|
||||
* URL: /board/[boardCode]
|
||||
*
|
||||
* Note: /boards/[boardCode]/page.tsx와 동일한 기능
|
||||
* 라우팅 일관성을 위해 singular 경로에도 페이지 추가
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter, useParams, useSearchParams } from 'next/navigation';
|
||||
import { MessageSquare, Plus } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
UniversalListPage,
|
||||
type UniversalListConfig,
|
||||
type TableColumn,
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||
import { getDynamicBoardPosts } from '@/components/board/DynamicBoard/actions';
|
||||
import { getBoardByCode } from '@/components/board/BoardManagement/actions';
|
||||
import { BoardDetail } from '@/components/board/BoardDetail';
|
||||
import type { PostApiData } from '@/components/customer-center/shared/types';
|
||||
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
|
||||
// 정렬 옵션
|
||||
const SORT_OPTIONS = [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '오래된순' },
|
||||
];
|
||||
|
||||
// 게시글 상태 옵션
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'published', label: '게시됨' },
|
||||
{ value: 'draft', label: '임시저장' },
|
||||
];
|
||||
|
||||
interface BoardPost {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
status: string;
|
||||
views: number;
|
||||
isNotice: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// API 데이터 → 프론트엔드 타입 변환
|
||||
function transformApiToPost(apiData: PostApiData): BoardPost {
|
||||
return {
|
||||
id: String(apiData.id),
|
||||
title: apiData.title,
|
||||
content: apiData.content,
|
||||
authorId: String(apiData.user_id),
|
||||
authorName: apiData.author?.name || '회원',
|
||||
status: apiData.status,
|
||||
views: apiData.views,
|
||||
isNotice: apiData.is_notice,
|
||||
createdAt: apiData.created_at,
|
||||
updatedAt: apiData.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export default function BoardCodePage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const boardCode = params.boardCode as string;
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// mode=new: 게시글 작성 폼 표시
|
||||
if (mode === 'new') {
|
||||
return <BoardDetail boardCode={boardCode} mode="create" />;
|
||||
}
|
||||
|
||||
// 게시판 정보
|
||||
const [boardName, setBoardName] = useState<string>('게시판');
|
||||
const [boardDescription, setBoardDescription] = useState<string>('');
|
||||
|
||||
// 게시글 목록
|
||||
const [posts, setPosts] = useState<BoardPost[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 필터 및 검색
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [sortOption, setSortOption] = useState<string>('latest');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
|
||||
// 게시판 정보 로드
|
||||
useEffect(() => {
|
||||
async function fetchBoardInfo() {
|
||||
const result = await getBoardByCode(boardCode);
|
||||
if (result.success && result.data) {
|
||||
setBoardName(result.data.boardName);
|
||||
setBoardDescription(result.data.description || '');
|
||||
}
|
||||
}
|
||||
fetchBoardInfo();
|
||||
}, [boardCode]);
|
||||
|
||||
// 게시글 목록 로드
|
||||
useEffect(() => {
|
||||
async function fetchPosts() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const result = await getDynamicBoardPosts(boardCode, { per_page: 100 });
|
||||
|
||||
if (result.success && result.data) {
|
||||
const transformed = result.data.data.map(transformApiToPost);
|
||||
setPosts(transformed);
|
||||
} else {
|
||||
setError(result.error || '게시글 목록을 불러오는데 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
fetchPosts();
|
||||
}, [boardCode]);
|
||||
|
||||
// 필터링 및 정렬
|
||||
const filteredData = useMemo(() => {
|
||||
let result = [...posts];
|
||||
|
||||
// 상태 필터
|
||||
if (statusFilter !== 'all') {
|
||||
result = result.filter((item) => item.status === statusFilter);
|
||||
}
|
||||
|
||||
// 날짜 필터
|
||||
if (startDate && endDate) {
|
||||
result = result.filter((item) => {
|
||||
const itemDate = format(new Date(item.createdAt), 'yyyy-MM-dd');
|
||||
return itemDate >= startDate && itemDate <= endDate;
|
||||
});
|
||||
}
|
||||
|
||||
// 검색 필터
|
||||
if (searchValue) {
|
||||
const searchLower = searchValue.toLowerCase();
|
||||
result = result.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(searchLower) ||
|
||||
item.authorName.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
if (sortOption === 'latest') {
|
||||
result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
} else {
|
||||
result.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [posts, statusFilter, startDate, endDate, searchValue, sortOption]);
|
||||
|
||||
// 페이지네이션
|
||||
const paginatedData = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
return filteredData.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
||||
}, [filteredData, currentPage]);
|
||||
|
||||
const totalPages = Math.ceil(filteredData.length / ITEMS_PER_PAGE);
|
||||
|
||||
// 핸들러
|
||||
const handleToggleSelection = useCallback((id: string) => {
|
||||
setSelectedItems((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleSelectAll = useCallback(() => {
|
||||
if (selectedItems.size === paginatedData.length) {
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
setSelectedItems(new Set(paginatedData.map((item) => item.id)));
|
||||
}
|
||||
}, [selectedItems.size, paginatedData]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(item: BoardPost) => {
|
||||
router.push(`/ko/board/${boardCode}/${item.id}?mode=view`);
|
||||
},
|
||||
[router, boardCode]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push(`/ko/board/${boardCode}?mode=new`);
|
||||
}, [router, boardCode]);
|
||||
|
||||
// 상태 Badge
|
||||
const getStatusBadge = (status: string) => {
|
||||
if (status === 'published') {
|
||||
return <Badge variant="secondary" className="bg-green-100 text-green-700">게시됨</Badge>;
|
||||
}
|
||||
if (status === 'draft') {
|
||||
return <Badge variant="secondary" className="bg-yellow-100 text-yellow-700">임시저장</Badge>;
|
||||
}
|
||||
return <Badge variant="secondary">{status}</Badge>;
|
||||
};
|
||||
|
||||
// 테이블 컬럼
|
||||
const tableColumns: TableColumn[] = [
|
||||
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
|
||||
{ key: 'title', label: '제목', className: 'min-w-[200px]' },
|
||||
{ key: 'author', label: '작성자', className: 'w-[120px]' },
|
||||
{ key: 'views', label: '조회수', className: 'w-[80px] text-center' },
|
||||
{ key: 'status', label: '상태', className: 'w-[100px] text-center' },
|
||||
{ key: 'createdAt', label: '등록일', className: 'w-[120px] text-center' },
|
||||
];
|
||||
|
||||
// 테이블 행 렌더링
|
||||
const renderTableRow = useCallback(
|
||||
(
|
||||
item: BoardPost,
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void }
|
||||
) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={onToggle}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{globalIndex}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.isNotice && (
|
||||
<Badge variant="destructive" className="text-xs">공지</Badge>
|
||||
)}
|
||||
{item.title}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.authorName}</TableCell>
|
||||
<TableCell className="text-center">{item.views}</TableCell>
|
||||
<TableCell className="text-center">{getStatusBadge(item.status)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{format(new Date(item.createdAt), 'yyyy-MM-dd')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
[handleRowClick]
|
||||
);
|
||||
|
||||
// 모바일 카드 렌더링
|
||||
const renderMobileCard = useCallback(
|
||||
(
|
||||
item: BoardPost,
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void }
|
||||
) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
return (
|
||||
<Card
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={isSelected} onCheckedChange={onToggle} />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">#{globalIndex}</span>
|
||||
{item.isNotice && (
|
||||
<Badge variant="destructive" className="text-xs">공지</Badge>
|
||||
)}
|
||||
</div>
|
||||
{getStatusBadge(item.status)}
|
||||
</div>
|
||||
<h3 className="font-medium">{item.title}</h3>
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>{item.authorName}</span>
|
||||
<span>{format(new Date(item.createdAt), 'yyyy-MM-dd')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
[handleRowClick]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// UniversalListPage 설정
|
||||
const boardConfig: UniversalListConfig<BoardPost> = {
|
||||
title: boardName,
|
||||
description: boardDescription || `${boardName} 게시판입니다.`,
|
||||
icon: MessageSquare,
|
||||
basePath: `/board/${boardCode}`,
|
||||
idField: 'id',
|
||||
|
||||
actions: {
|
||||
getList: async () => ({
|
||||
success: true,
|
||||
data: filteredData,
|
||||
totalCount: filteredData.length,
|
||||
}),
|
||||
},
|
||||
|
||||
columns: tableColumns,
|
||||
headerActions: () => (
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<Button className="ml-auto" onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
글쓰기
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
총 {filteredData.length}건
|
||||
</span>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onValueChange={setStatusFilter}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={sortOption} onValueChange={setSortOption}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
|
||||
searchPlaceholder: '제목, 작성자로 검색...',
|
||||
itemsPerPage: ITEMS_PER_PAGE,
|
||||
clientSideFiltering: true,
|
||||
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
};
|
||||
|
||||
return (
|
||||
<UniversalListPage<BoardPost>
|
||||
config={boardConfig}
|
||||
initialData={filteredData}
|
||||
initialTotalCount={filteredData.length}
|
||||
externalSelection={{
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
toggleSelection: handleToggleSelection,
|
||||
toggleSelectAll: handleToggleSelectAll,
|
||||
}}
|
||||
externalSearch={{
|
||||
searchTerm: searchValue,
|
||||
setSearchTerm: setSearchValue,
|
||||
}}
|
||||
externalPagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems: filteredData.length,
|
||||
itemsPerPage: ITEMS_PER_PAGE,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { BoardManagement } from '@/components/board/BoardManagement';
|
||||
import { BoardDetailClientV2 } from '@/components/board/BoardManagement/BoardDetailClientV2';
|
||||
|
||||
export default function BoardManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <BoardDetailClientV2 boardId="new" />;
|
||||
}
|
||||
|
||||
return <BoardManagement />;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function DynamicBoardDetailPage() {
|
||||
|
||||
// 수정 페이지로 이동
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/boards/${boardCode}/${postId}/edit`);
|
||||
router.push(`/ko/boards/${boardCode}/${postId}?mode=edit`);
|
||||
}, [router, boardCode, postId]);
|
||||
|
||||
// 목록으로 이동
|
||||
|
||||
@@ -205,7 +205,7 @@ export default function DynamicBoardListPage() {
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push(`/ko/boards/${boardCode}/create`);
|
||||
router.push(`/ko/boards/${boardCode}?mode=new`);
|
||||
}, [router, boardCode]);
|
||||
|
||||
// 상태 Badge
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { ItemManagementClient } from '@/components/business/construction/item-management';
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ItemManagementClient, ItemDetailClient } from '@/components/business/construction/item-management';
|
||||
|
||||
export default function ItemManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <ItemDetailClient isNewMode />;
|
||||
}
|
||||
|
||||
return <ItemManagementClient />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { LaborManagementClient } from '@/components/business/construction/labor-management';
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { LaborManagementClient, LaborDetailClientV2 } from '@/components/business/construction/labor-management';
|
||||
|
||||
export default function LaborManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <LaborDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
|
||||
return <LaborManagementClient />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import PricingListClient from '@/components/business/construction/pricing-management/PricingListClient';
|
||||
import { PricingDetailClientV2 } from '@/components/business/construction/pricing-management';
|
||||
|
||||
export default function PricingPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <PricingDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
|
||||
return <PricingListClient />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { OrderManagementListClient } from '@/components/business/construction/order-management';
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { OrderManagementListClient, OrderDetailForm } from '@/components/business/construction/order-management';
|
||||
|
||||
export default function OrderManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <OrderDetailForm mode="create" />;
|
||||
}
|
||||
|
||||
return <OrderManagementListClient />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import StructureReviewListClient from '@/components/business/construction/structure-review/StructureReviewListClient';
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { StructureReviewListClient, StructureReviewDetailForm } from '@/components/business/construction/structure-review';
|
||||
|
||||
export default function StructureReviewListPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <StructureReviewDetailForm mode="new" />;
|
||||
}
|
||||
|
||||
return <StructureReviewListClient />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { PartnerListClient } from '@/components/business/construction/partners';
|
||||
import PartnerForm from '@/components/business/construction/partners/PartnerForm';
|
||||
|
||||
export default function PartnersPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <PartnerForm mode="new" />;
|
||||
}
|
||||
|
||||
return <PartnerListClient />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { SiteBriefingListClient } from '@/components/business/construction/site-briefings';
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { SiteBriefingListClient, SiteBriefingForm } from '@/components/business/construction/site-briefings';
|
||||
|
||||
export default function SiteBriefingsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <SiteBriefingForm mode="new" />;
|
||||
}
|
||||
|
||||
return <SiteBriefingListClient />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import ProjectDetailClient from '@/components/business/construction/management/ProjectDetailClient';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
locale: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function ProjectExecutionManagementPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <ProjectDetailClient projectId={id} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import ProjectDetailClient from '@/components/business/construction/management/ProjectDetailClient';
|
||||
|
||||
export default function ProjectExecutionManagementPage() {
|
||||
// projectId 없이 호출 → 전체 데이터 칸반보드
|
||||
return <ProjectDetailClient />;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import IssueManagementListClient from '@/components/business/construction/issue-management/IssueManagementListClient';
|
||||
import IssueDetailForm from '@/components/business/construction/issue-management/IssueDetailForm';
|
||||
import {
|
||||
getIssueList,
|
||||
getIssueStats,
|
||||
@@ -12,33 +14,44 @@ import type {
|
||||
} from '@/components/business/construction/issue-management/types';
|
||||
|
||||
export default function IssueManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Issue[]>([]);
|
||||
const [stats, setStats] = useState<IssueStats | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [listResult, statsResult] = await Promise.all([
|
||||
getIssueList({ size: 1000 }),
|
||||
getIssueStats(),
|
||||
]);
|
||||
if (mode !== 'new') {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [listResult, statsResult] = await Promise.all([
|
||||
getIssueList({ size: 1000 }),
|
||||
getIssueStats(),
|
||||
]);
|
||||
|
||||
if (listResult.success && listResult.data) {
|
||||
setData(listResult.data.items);
|
||||
if (listResult.success && listResult.data) {
|
||||
setData(listResult.data.items);
|
||||
}
|
||||
if (statsResult.success && statsResult.data) {
|
||||
setStats(statsResult.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load issue management data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
if (statsResult.success && statsResult.data) {
|
||||
setStats(statsResult.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load issue management data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
loadData();
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
if (mode === 'new') {
|
||||
return <IssueDetailForm mode="create" />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 1:1 문의 목록 페이지
|
||||
* URL: /customer-center/qna
|
||||
* - ?mode=new: 문의 등록
|
||||
*/
|
||||
|
||||
import { InquiryList } from '@/components/customer-center/InquiryManagement';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { InquiryList, InquiryDetailClientV2 } from '@/components/customer-center/InquiryManagement';
|
||||
|
||||
export default function InquiriesPage() {
|
||||
return <InquiryList />;
|
||||
}
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
export const metadata = {
|
||||
title: '1:1 문의',
|
||||
description: '1:1 문의를 등록하고 답변을 확인합니다.',
|
||||
};
|
||||
// mode=new: 문의 등록
|
||||
if (mode === 'new') {
|
||||
return <InquiryDetailClientV2 />;
|
||||
}
|
||||
|
||||
return <InquiryList />;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
CreditAnalysisModal,
|
||||
MOCK_CREDIT_DATA,
|
||||
} from '@/components/accounting/VendorManagement/CreditAnalysisModal';
|
||||
|
||||
export default function CreditAnalysisTestPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleApprove = () => {
|
||||
toast.success('거래가 승인되었습니다.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>기업 신용분석 모달 테스트</CardTitle>
|
||||
<CardDescription>
|
||||
신규 거래처 등록 시 표시되는 신용분석 모달을 테스트합니다.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="p-4 bg-gray-50 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">목업 데이터 정보</h3>
|
||||
<ul className="text-sm text-gray-600 space-y-1">
|
||||
<li>사업자번호: {MOCK_CREDIT_DATA.businessNumber}</li>
|
||||
<li>법인명: {MOCK_CREDIT_DATA.companyName}</li>
|
||||
<li>신용등급: Level {MOCK_CREDIT_DATA.creditLevel} ({MOCK_CREDIT_DATA.creditStatus})</li>
|
||||
<li>거래 승인: {MOCK_CREDIT_DATA.approval.safety}</li>
|
||||
<li>외상 가능: {MOCK_CREDIT_DATA.approval.creditAvailable ? '가능' : '불가'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => setIsModalOpen(true)}>
|
||||
신용분석 모달 열기
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<CreditAnalysisModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
data={MOCK_CREDIT_DATA}
|
||||
onApprove={handleApprove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
272
src/app/[locale]/(protected)/hr/documents/page.tsx
Normal file
272
src/app/[locale]/(protected)/hr/documents/page.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, Suspense } from 'react';
|
||||
import { FileText, ArrowLeft, Calendar, Clock, MapPin, FileCheck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { FormSectionSkeleton } from '@/components/ui/skeleton';
|
||||
import { format } from 'date-fns';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// 문서 유형 라벨
|
||||
const DOCUMENT_TYPE_LABELS: Record<string, string> = {
|
||||
businessTripRequest: '출장신청',
|
||||
vacationRequest: '휴가신청',
|
||||
fieldWorkRequest: '외근신청',
|
||||
overtimeRequest: '연장근무신청',
|
||||
};
|
||||
|
||||
// 문서 유형 아이콘
|
||||
const DOCUMENT_TYPE_ICONS: Record<string, React.ElementType> = {
|
||||
businessTripRequest: MapPin,
|
||||
vacationRequest: Calendar,
|
||||
fieldWorkRequest: MapPin,
|
||||
overtimeRequest: Clock,
|
||||
};
|
||||
|
||||
// 문서 유형 설명
|
||||
const DOCUMENT_TYPE_DESCRIPTIONS: Record<string, string> = {
|
||||
businessTripRequest: '업무상 출장이 필요한 경우 작성합니다',
|
||||
vacationRequest: '연차, 병가 등 휴가 신청 시 작성합니다',
|
||||
fieldWorkRequest: '사업장 외 근무가 필요한 경우 작성합니다',
|
||||
overtimeRequest: '정규 근무시간 외 연장근무 시 작성합니다',
|
||||
};
|
||||
|
||||
function DocumentNewContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const documentType = searchParams.get('type') || 'businessTripRequest';
|
||||
|
||||
// 폼 상태
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
startDate: format(new Date(), 'yyyy-MM-dd'),
|
||||
endDate: format(new Date(), 'yyyy-MM-dd'),
|
||||
startTime: '09:00',
|
||||
endTime: '18:00',
|
||||
destination: '',
|
||||
purpose: '',
|
||||
content: '',
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 문서 유형 정보
|
||||
const typeInfo = useMemo(() => ({
|
||||
label: DOCUMENT_TYPE_LABELS[documentType] || '문서 등록',
|
||||
description: DOCUMENT_TYPE_DESCRIPTIONS[documentType] || '문서를 작성합니다',
|
||||
Icon: DOCUMENT_TYPE_ICONS[documentType] || FileText,
|
||||
}), [documentType]);
|
||||
|
||||
// 입력 핸들러
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// 제출 핸들러
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// TODO: 백엔드 API 구현 필요
|
||||
toast.success(`${typeInfo.label}이 등록되었습니다`);
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.error('Document creation error:', error);
|
||||
toast.error('문서 등록에 실패했습니다');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 취소 핸들러
|
||||
const handleCancel = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 max-w-2xl">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCancel}
|
||||
className="mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
근태관리로 돌아가기
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<typeInfo.Icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{typeInfo.label}</h1>
|
||||
<p className="text-muted-foreground">{typeInfo.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 폼 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileCheck className="w-5 h-5" />
|
||||
신청 정보
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
필수 정보를 입력해주세요
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* 제목 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">제목</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder={`${typeInfo.label} 제목을 입력하세요`}
|
||||
value={formData.title}
|
||||
onChange={(e) => handleChange('title', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 날짜 (출장/휴가/외근) */}
|
||||
{['businessTripRequest', 'vacationRequest', 'fieldWorkRequest'].includes(documentType) && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="startDate">시작일</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
type="date"
|
||||
value={formData.startDate}
|
||||
onChange={(e) => handleChange('startDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endDate">종료일</Label>
|
||||
<Input
|
||||
id="endDate"
|
||||
type="date"
|
||||
value={formData.endDate}
|
||||
onChange={(e) => handleChange('endDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 시간 (연장근무) */}
|
||||
{documentType === 'overtimeRequest' && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="startTime">시작 시간</Label>
|
||||
<Input
|
||||
id="startTime"
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => handleChange('startTime', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endTime">종료 시간</Label>
|
||||
<Input
|
||||
id="endTime"
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => handleChange('endTime', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 목적지/장소 (출장/외근) */}
|
||||
{['businessTripRequest', 'fieldWorkRequest'].includes(documentType) && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="destination">
|
||||
{documentType === 'businessTripRequest' ? '출장지' : '외근 장소'}
|
||||
</Label>
|
||||
<Input
|
||||
id="destination"
|
||||
placeholder="장소를 입력하세요"
|
||||
value={formData.destination}
|
||||
onChange={(e) => handleChange('destination', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 목적/사유 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="purpose">
|
||||
{documentType === 'vacationRequest' ? '휴가 사유' : '목적'}
|
||||
</Label>
|
||||
<Input
|
||||
id="purpose"
|
||||
placeholder={documentType === 'vacationRequest' ? '휴가 사유를 입력하세요' : '목적을 입력하세요'}
|
||||
value={formData.purpose}
|
||||
onChange={(e) => handleChange('purpose', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 상세 내용 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="content">상세 내용</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
placeholder="상세 내용을 입력하세요"
|
||||
rows={4}
|
||||
value={formData.content}
|
||||
onChange={(e) => handleChange('content', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 버튼 */}
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? '등록 중...' : '등록'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DocumentListContent() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="text-center py-12">
|
||||
<FileText className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">HR 문서 관리</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
근태관리에서 사유 등록 시 문서를 작성할 수 있습니다.
|
||||
</p>
|
||||
<Button onClick={() => router.push('/hr/attendance')}>
|
||||
근태관리로 이동
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DocumentsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return (
|
||||
<Suspense fallback={<FormSectionSkeleton rows={6} />}>
|
||||
<DocumentNewContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <DocumentListContent />;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export default function EmployeeDetailPage() {
|
||||
try {
|
||||
const result = await updateEmployee(id, data);
|
||||
if (result.success) {
|
||||
router.push(`/ko/hr/employee-management/${id}`);
|
||||
router.push(`/ko/hr/employee-management/${id}?mode=view`);
|
||||
} else {
|
||||
console.error('[EmployeeDetailPage] Update failed:', result.error);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,68 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 사원관리 페이지 (Employee Management)
|
||||
*
|
||||
* 사원 정보를 관리하는 시스템
|
||||
* - 사원 목록 조회/검색/필터
|
||||
* - 사원 등록/수정/삭제
|
||||
* - CSV 일괄 등록
|
||||
* - 사용자 초대
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { useSearchParams, useRouter, useParams } from 'next/navigation';
|
||||
import { EmployeeManagement } from '@/components/hr/EmployeeManagement';
|
||||
import { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm';
|
||||
import { createEmployee } from '@/components/hr/EmployeeManagement/actions';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
import type { Metadata } from 'next';
|
||||
import { toast } from 'sonner';
|
||||
import type { EmployeeFormData } from '@/components/hr/EmployeeManagement/types';
|
||||
|
||||
/**
|
||||
* 메타데이터 설정
|
||||
*/
|
||||
export const metadata: Metadata = {
|
||||
title: '사원관리',
|
||||
description: '사원 정보를 관리합니다',
|
||||
};
|
||||
function formatErrorMessage(result: {
|
||||
error?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
status?: number;
|
||||
}): string {
|
||||
const parts: string[] = [];
|
||||
if (result.status) parts.push(`[${result.status}]`);
|
||||
if (result.error) parts.push(result.error);
|
||||
if (result.errors) {
|
||||
const errorMessages = Object.entries(result.errors)
|
||||
.map(([field, messages]) => `${field}: ${messages[0]}`)
|
||||
.join(', ');
|
||||
if (errorMessages) parts.push(`(${errorMessages})`);
|
||||
}
|
||||
return parts.join(' ') || '알 수 없는 오류가 발생했습니다.';
|
||||
}
|
||||
|
||||
function EmployeeManagementContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const mode = searchParams.get('mode');
|
||||
const locale = params.locale as string || 'ko';
|
||||
|
||||
if (mode === 'new') {
|
||||
const handleSave = async (data: EmployeeFormData) => {
|
||||
try {
|
||||
const result = await createEmployee(data);
|
||||
if (result.success) {
|
||||
toast.success('사원이 등록되었습니다.');
|
||||
router.push(`/${locale}/hr/employee-management`);
|
||||
} else {
|
||||
const errorMessage = formatErrorMessage(result);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('서버 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
return <EmployeeForm mode="create" onSave={handleSave} />;
|
||||
}
|
||||
|
||||
return <EmployeeManagement />;
|
||||
}
|
||||
|
||||
export default function EmployeeManagementPage() {
|
||||
return (
|
||||
<Suspense fallback={<ListPageSkeleton showHeader={false} showStats={true} statsCount={4} />}>
|
||||
<EmployeeManagement />
|
||||
<EmployeeManagementContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 공정 목록 페이지
|
||||
* 공정 목록/등록 페이지
|
||||
*/
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import ProcessListClient from '@/components/process-management/ProcessListClient';
|
||||
import { ProcessDetailClientV2 } from '@/components/process-management';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '공정 목록',
|
||||
description: '공정 관리 - 생산 공정 목록 조회 및 관리',
|
||||
};
|
||||
function ProcessManagementContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <ProcessDetailClientV2 processId="new" />;
|
||||
}
|
||||
|
||||
return <ProcessListClient />;
|
||||
}
|
||||
|
||||
export default function ProcessManagementPage() {
|
||||
return (
|
||||
<Suspense fallback={<ListPageSkeleton showHeader={false} />}>
|
||||
<ProcessListClient />
|
||||
<ProcessManagementContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출하관리 - 목록 페이지
|
||||
* 출하관리 - 목록/등록 페이지
|
||||
* URL: /outbound/shipments
|
||||
* URL: /outbound/shipments?mode=new
|
||||
*/
|
||||
|
||||
import { ShipmentList } from '@/components/outbound/ShipmentManagement';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ShipmentList, ShipmentCreate } from '@/components/outbound/ShipmentManagement';
|
||||
|
||||
export default function ShipmentsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <ShipmentCreate />;
|
||||
}
|
||||
|
||||
return <ShipmentList />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,104 @@
|
||||
* 품목기준관리 API 연동
|
||||
* - 품목 목록: API에서 조회
|
||||
* - 테이블 컬럼: custom-tabs API에서 동적 구성
|
||||
* - ?mode=new: 등록 화면으로 전환
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import ItemListClient from '@/components/items/ItemListClient';
|
||||
import DynamicItemForm from '@/components/items/DynamicItemForm';
|
||||
import type { DynamicFormData } from '@/components/items/DynamicItemForm/types';
|
||||
import { DuplicateCodeError } from '@/lib/api/error-handler';
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* 품목 목록 페이지
|
||||
* 품목 목록 페이지 (mode=new 시 등록 화면)
|
||||
*/
|
||||
export default function ItemsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// mode=new 일 때 등록 화면 렌더링
|
||||
if (mode === 'new') {
|
||||
return <CreateItemContent />;
|
||||
}
|
||||
|
||||
return <ItemListClient />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메타데이터 설정
|
||||
* 품목 등록 컴포넌트 (mode=new용)
|
||||
*/
|
||||
export const metadata = {
|
||||
title: '품목 관리',
|
||||
description: '품목 목록 조회 및 관리',
|
||||
};
|
||||
function CreateItemContent() {
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (data: DynamicFormData) => {
|
||||
setSubmitError(null);
|
||||
|
||||
// 필드명 변환: spec → specification (백엔드 API 규격)
|
||||
const submitData = { ...data };
|
||||
if (submitData.spec !== undefined) {
|
||||
submitData.specification = submitData.spec;
|
||||
delete submitData.spec;
|
||||
}
|
||||
|
||||
// item_type 설정
|
||||
const itemType = submitData.product_type as string;
|
||||
submitData.item_type = itemType;
|
||||
|
||||
// 이미지 데이터 제거 (파일 업로드는 별도 API 사용)
|
||||
if (submitData.bending_diagram && typeof submitData.bending_diagram === 'string' && (submitData.bending_diagram as string).startsWith('data:')) {
|
||||
delete submitData.bending_diagram;
|
||||
}
|
||||
if (submitData.specification_file && typeof submitData.specification_file === 'string' && (submitData.specification_file as string).startsWith('data:')) {
|
||||
delete submitData.specification_file;
|
||||
}
|
||||
if (submitData.certification_file && typeof submitData.certification_file === 'string' && (submitData.certification_file as string).startsWith('data:')) {
|
||||
delete submitData.certification_file;
|
||||
}
|
||||
|
||||
// API 호출: POST /api/proxy/items
|
||||
const response = await fetch('/api/proxy/items', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
if (response.status === 400 && result.duplicate_id) {
|
||||
throw new DuplicateCodeError(
|
||||
result.message || '해당 품목코드가 이미 존재합니다.',
|
||||
result.duplicate_id,
|
||||
result.duplicate_code
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage = result.message || '품목 등록에 실패했습니다.';
|
||||
setSubmitError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return { id: result.data.id, ...result.data };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{submitError && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-900">
|
||||
⚠️ {submitError}
|
||||
</div>
|
||||
)}
|
||||
<DynamicItemForm
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 작업지시 목록 페이지
|
||||
* URL: /production/work-orders
|
||||
* - ?mode=new: 작업지시 등록
|
||||
*/
|
||||
|
||||
import { WorkOrderList } from '@/components/production/WorkOrders';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { WorkOrderList, WorkOrderCreate } from '@/components/production/WorkOrders';
|
||||
|
||||
export default function WorkOrdersPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// mode=new: 작업지시 등록
|
||||
if (mode === 'new') {
|
||||
return <WorkOrderCreate />;
|
||||
}
|
||||
|
||||
return <WorkOrderList />;
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 검사 목록 페이지
|
||||
* 검사 목록/등록 페이지
|
||||
* URL: /quality/inspections
|
||||
* URL: /quality/inspections?mode=new
|
||||
*/
|
||||
|
||||
import { InspectionList } from '@/components/quality/InspectionManagement';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { InspectionList, InspectionCreate } from '@/components/quality/InspectionManagement';
|
||||
|
||||
export default function InspectionsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <InspectionCreate />;
|
||||
}
|
||||
|
||||
return <InspectionList />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect, useCallback, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { ClientDetailClientV2 } from '@/components/clients/ClientDetailClientV2';
|
||||
import { useClientList, Client } from "@/hooks/useClientList";
|
||||
import {
|
||||
Building2,
|
||||
@@ -52,9 +53,11 @@ import { isNextRedirectError } from "@/lib/utils/redirect-error";
|
||||
|
||||
export default function CustomerAccountManagementPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// API 훅 사용
|
||||
// API 훅 사용 (훅은 조건부 return 전에 항상 호출되어야 함 - React 훅 규칙)
|
||||
const {
|
||||
clients,
|
||||
pagination,
|
||||
@@ -261,15 +264,15 @@ export default function CustomerAccountManagementPage() {
|
||||
|
||||
// 핸들러 - 페이지 기반 네비게이션
|
||||
const handleAddNew = () => {
|
||||
router.push("/sales/client-management-sales-admin/new");
|
||||
router.push("/sales/client-management-sales-admin?mode=new");
|
||||
};
|
||||
|
||||
const handleEdit = (customer: Client) => {
|
||||
router.push(`/sales/client-management-sales-admin/${customer.id}/edit`);
|
||||
router.push(`/sales/client-management-sales-admin/${customer.id}?mode=edit`);
|
||||
};
|
||||
|
||||
const handleView = (customer: Client) => {
|
||||
router.push(`/sales/client-management-sales-admin/${customer.id}`);
|
||||
router.push(`/sales/client-management-sales-admin/${customer.id}?mode=view`);
|
||||
};
|
||||
|
||||
const handleDelete = (customerId: string) => {
|
||||
@@ -354,6 +357,11 @@ export default function CustomerAccountManagementPage() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// mode=new 처리 (모든 훅 호출 후에 조건부 return - React 훅 규칙 준수)
|
||||
if (mode === 'new') {
|
||||
return <ClientDetailClientV2 />;
|
||||
}
|
||||
|
||||
// 상태 뱃지
|
||||
const getStatusBadge = (status: "활성" | "비활성") => {
|
||||
if (status === "활성") {
|
||||
|
||||
@@ -99,12 +99,15 @@ function getOrderStatusBadge(status: OrderStatus) {
|
||||
order_confirmed: { label: "수주확정", className: "bg-gray-100 text-gray-700 border-gray-200" },
|
||||
production_ordered: { label: "생산지시완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
|
||||
in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" },
|
||||
produced: { label: "생산완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
|
||||
rework: { label: "재작업중", className: "bg-orange-100 text-orange-700 border-orange-200" },
|
||||
work_completed: { label: "작업완료", className: "bg-blue-600 text-white border-blue-600" },
|
||||
shipping: { label: "출하중", className: "bg-purple-100 text-purple-700 border-purple-200" },
|
||||
shipped: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" },
|
||||
completed: { label: "완료", className: "bg-gray-500 text-white border-gray-500" },
|
||||
cancelled: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" },
|
||||
};
|
||||
const config = statusConfig[status];
|
||||
const config = statusConfig[status] || { label: status, className: "bg-gray-100 text-gray-700 border-gray-200" };
|
||||
return (
|
||||
<BadgeSm className={config.className}>
|
||||
{config.label}
|
||||
@@ -535,7 +538,7 @@ export default function OrderEditPage() {
|
||||
// Edit mode config override
|
||||
const editConfig = {
|
||||
...orderSalesConfig,
|
||||
title: '수주 수정',
|
||||
title: '수주',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
* - 기본 정보, 수주/배송 정보, 비고
|
||||
* - 제품 내역 테이블
|
||||
* - 상태별 버튼 차이
|
||||
* - ?mode=edit: 수정 모드 (V2 패턴)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { useRouter, useParams, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
@@ -79,12 +80,15 @@ function getOrderStatusBadge(status: OrderStatus) {
|
||||
order_confirmed: { label: "수주확정", className: "bg-gray-100 text-gray-700 border-gray-200" },
|
||||
production_ordered: { label: "생산지시완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
|
||||
in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" },
|
||||
produced: { label: "생산완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
|
||||
rework: { label: "재작업중", className: "bg-orange-100 text-orange-700 border-orange-200" },
|
||||
work_completed: { label: "작업완료", className: "bg-blue-600 text-white border-blue-600" },
|
||||
shipping: { label: "출하중", className: "bg-purple-100 text-purple-700 border-purple-200" },
|
||||
shipped: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" },
|
||||
completed: { label: "완료", className: "bg-gray-500 text-white border-gray-500" },
|
||||
cancelled: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" },
|
||||
};
|
||||
const config = statusConfig[status];
|
||||
const config = statusConfig[status] || { label: status, className: "bg-gray-100 text-gray-700 border-gray-200" };
|
||||
return (
|
||||
<BadgeSm className={config.className}>
|
||||
{config.label}
|
||||
@@ -105,7 +109,12 @@ function InfoItem({ label, value }: { label: string; value?: string }) {
|
||||
export default function OrderDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const orderId = params.id as string;
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// V2 패턴: mode 파라미터로 모드 결정
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [order, setOrder] = useState<Order | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -159,7 +168,8 @@ export default function OrderDetailPage() {
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/sales/order-management-sales/${orderId}/edit`);
|
||||
// V2 패턴: ?mode=edit로 이동
|
||||
router.push(`/sales/order-management-sales/${orderId}?mode=edit`);
|
||||
};
|
||||
|
||||
const handleProductionOrder = () => {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect, useCallback, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { OrderRegistration, OrderFormData, createOrder } from "@/components/orders";
|
||||
import {
|
||||
FileText,
|
||||
Edit,
|
||||
@@ -79,7 +80,49 @@ function getOrderStatusBadge(status: OrderStatus) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 등록 컴포넌트 (mode=new용)
|
||||
*/
|
||||
function CreateOrderContent() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleBack = () => {
|
||||
router.push("/sales/order-management-sales");
|
||||
};
|
||||
|
||||
const handleSave = async (formData: OrderFormData) => {
|
||||
try {
|
||||
const result = await createOrder(formData);
|
||||
if (result.success) {
|
||||
toast.success("수주가 등록되었습니다.");
|
||||
router.push("/sales/order-management-sales");
|
||||
} else {
|
||||
toast.error(result.error || "수주 등록에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("수주 등록 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
return <OrderRegistration onBack={handleBack} onSave={handleSave} />;
|
||||
}
|
||||
|
||||
export default function OrderManagementSalesPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
// mode=new 처리: 별도 컴포넌트로 분리하여 Hooks 규칙 준수
|
||||
if (mode === 'new') {
|
||||
return <CreateOrderContent />;
|
||||
}
|
||||
|
||||
return <OrderListContent />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 목록 컴포넌트 (기본 화면)
|
||||
*/
|
||||
function OrderListContent() {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
@@ -265,11 +308,11 @@ export default function OrderManagementSalesPage() {
|
||||
|
||||
// 핸들러
|
||||
const handleView = (order: Order) => {
|
||||
router.push(`/sales/order-management-sales/${order.id}`);
|
||||
router.push(`/sales/order-management-sales/${order.id}?mode=view`);
|
||||
};
|
||||
|
||||
const handleEdit = (order: Order) => {
|
||||
router.push(`/sales/order-management-sales/${order.id}/edit`);
|
||||
router.push(`/sales/order-management-sales/${order.id}?mode=edit`);
|
||||
};
|
||||
|
||||
// 개별 취소 기능은 상세 페이지에서 처리
|
||||
@@ -690,7 +733,7 @@ export default function OrderManagementSalesPage() {
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
수주완료
|
||||
</Button>
|
||||
<Button onClick={() => router.push("/sales/order-management-sales/new")}>
|
||||
<Button onClick={() => router.push("/sales/order-management-sales?mode=new")}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
수주 등록
|
||||
</Button>
|
||||
|
||||
@@ -13,10 +13,14 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { PricingListClient } from '@/components/pricing';
|
||||
import { getPricingListData, type PricingListItem } from '@/components/pricing/actions';
|
||||
|
||||
export default function PricingManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const mode = searchParams.get('mode');
|
||||
const [data, setData] = useState<PricingListItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@@ -28,6 +32,27 @@ export default function PricingManagementPage() {
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
// mode=new: 단가 등록은 품목 선택이 필요하므로 안내 표시
|
||||
if (mode === 'new') {
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4">
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-xl font-semibold mb-2">품목을 선택해주세요</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
단가 등록은 품목 선택이 필요합니다.<br />
|
||||
단가 목록에서 미등록 품목을 선택한 후 등록해주세요.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push('/sales/pricing-management')}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
단가 목록으로 이동
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
/**
|
||||
* 견적관리 페이지 (Client Component)
|
||||
*
|
||||
* 초기 데이터를 useEffect에서 fetch하여 Client Component에 전달
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { QuoteManagementClient } from '@/components/quotes/QuoteManagementClient';
|
||||
import { getQuotes } from '@/components/quotes/actions';
|
||||
import { QuoteRegistration, QuoteFormData } from '@/components/quotes/QuoteRegistration';
|
||||
import { getQuotes, createQuote, transformFormDataToApi } from '@/components/quotes';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
@@ -18,18 +19,56 @@ const DEFAULT_PAGINATION = {
|
||||
};
|
||||
|
||||
export default function QuoteManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getQuotes>>['data']>([]);
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getQuotes({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
if (mode !== 'new') {
|
||||
getQuotes({ perPage: 100 })
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
setPagination(result.pagination);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
if (mode === 'new') {
|
||||
const handleBack = () => {
|
||||
router.push('/sales/quote-management');
|
||||
};
|
||||
|
||||
const handleSave = async (formData: QuoteFormData): Promise<{ success: boolean; error?: string }> => {
|
||||
if (isSaving) return { success: false, error: '저장 중입니다.' };
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const apiData = transformFormDataToApi(formData);
|
||||
const result = await createQuote(apiData as any);
|
||||
|
||||
if (result.success && result.data) {
|
||||
router.push(`/sales/quote-management/${result.data.id}`);
|
||||
return { success: true };
|
||||
} else {
|
||||
return { success: false, error: result.error || '견적 등록에 실패했습니다.' };
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: '견적 등록에 실패했습니다.' };
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <QuoteRegistration onBack={handleBack} onSave={handleSave} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -45,4 +84,4 @@ export default function QuoteManagementPage() {
|
||||
initialPagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { AccountManagement } from '@/components/settings/AccountManagement';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { accountConfig } from '@/components/settings/AccountManagement/accountConfig';
|
||||
import { createBankAccount } from '@/components/settings/AccountManagement/actions';
|
||||
import type { AccountFormData } from '@/components/settings/AccountManagement/types';
|
||||
|
||||
export default function AccountsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
const handleSubmit = async (data: Record<string, unknown>) => {
|
||||
const result = await createBankAccount(data as AccountFormData);
|
||||
return { success: result.success, error: result.error };
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={accountConfig}
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AccountManagement />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { PermissionDetailClient } from '@/components/settings/PermissionManagement/PermissionDetailClient';
|
||||
|
||||
interface PageProps {
|
||||
@@ -9,5 +10,8 @@ interface PageProps {
|
||||
|
||||
export default function PermissionDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
return <PermissionDetailClient permissionId={id} />;
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode') as 'view' | 'edit' | null;
|
||||
|
||||
return <PermissionDetailClient permissionId={id} mode={mode || 'view'} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { PermissionManagement } from '@/components/settings/PermissionManagement';
|
||||
import { PermissionDetailClient } from '@/components/settings/PermissionManagement/PermissionDetailClient';
|
||||
|
||||
export default function PermissionsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
if (mode === 'new') {
|
||||
return <PermissionDetailClient permissionId="new" isNew />;
|
||||
}
|
||||
|
||||
return <PermissionManagement />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { PopupList } from '@/components/settings/PopupManagement';
|
||||
import { PopupDetailClientV2 } from '@/components/settings/PopupManagement/PopupDetailClientV2';
|
||||
import { getPopups } from '@/components/settings/PopupManagement/actions';
|
||||
import type { Popup } from '@/components/settings/PopupManagement/types';
|
||||
|
||||
export default function PopupManagementPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
const [data, setData] = useState<Popup[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getPopups({ size: 100 })
|
||||
.then(result => {
|
||||
setData(result);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
if (mode !== 'new') {
|
||||
getPopups({ size: 100 })
|
||||
.then(result => {
|
||||
setData(result);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
if (mode === 'new') {
|
||||
return <PopupDetailClientV2 popupId="new" />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -26,4 +39,4 @@ export default function PopupManagementPage() {
|
||||
}
|
||||
|
||||
return <PopupList initialData={data} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 193 192">
|
||||
<rect width="193" height="192" rx="24" fill="#3B82F6"/>
|
||||
<image x="0" y="0" width="193" height="192" xlink:href="data:image/png;base64,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"/>
|
||||
<defs>
|
||||
<clipPath id="rounded">
|
||||
<rect width="193" height="192" rx="40" ry="40"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded)">
|
||||
<rect width="193" height="192" fill="#3B82F6"/>
|
||||
<image x="0" y="0" width="193" height="192" xlink:href="data:image/png;base64,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"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -137,7 +137,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
if (isNewMode) {
|
||||
router.push('/ko/accounting/bad-debt-collection');
|
||||
} else {
|
||||
router.push(`/ko/accounting/bad-debt-collection/${recordId}`);
|
||||
router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=view`);
|
||||
}
|
||||
}, [router, recordId, isNewMode]);
|
||||
|
||||
@@ -163,7 +163,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
const result = await updateBadDebt(recordId!, formData);
|
||||
if (result.success) {
|
||||
toast.success('악성채권이 수정되었습니다.');
|
||||
router.push(`/ko/accounting/bad-debt-collection/${recordId}`);
|
||||
router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=view`);
|
||||
} else {
|
||||
toast.error(result.error || '수정에 실패했습니다.');
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: BadDebtRecord) => {
|
||||
router.push(`/ko/accounting/bad-debt-collection/${item.id}`);
|
||||
router.push(`/ko/accounting/bad-debt-collection/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -163,7 +163,7 @@ export function BillDetail({ billId, mode }: BillDetailProps) {
|
||||
if (isNewMode) {
|
||||
router.push('/ko/accounting/bills');
|
||||
} else {
|
||||
router.push(`/ko/accounting/bills/${billId}`);
|
||||
router.push(`/ko/accounting/bills/${billId}?mode=view`);
|
||||
}
|
||||
return { success: true };
|
||||
} else {
|
||||
@@ -434,10 +434,12 @@ export function BillDetail({ billId, mode }: BillDetailProps) {
|
||||
);
|
||||
|
||||
// ===== 템플릿 모드 및 동적 설정 =====
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "어음 상세"로 표시하려면 직접 설정 필요
|
||||
const templateMode = isNewMode ? 'create' : mode;
|
||||
const dynamicConfig = {
|
||||
...billConfig,
|
||||
title: isNewMode ? '어음 등록' : '어음 상세',
|
||||
title: isViewMode ? '어음 상세' : '어음',
|
||||
actions: {
|
||||
...billConfig.actions,
|
||||
submitLabel: isNewMode ? '등록' : '저장',
|
||||
|
||||
@@ -150,7 +150,7 @@ export function BillManagementClient({
|
||||
|
||||
// ===== 액션 핸들러 =====
|
||||
const handleRowClick = useCallback((item: BillRecord) => {
|
||||
router.push(`/ko/accounting/bills/${item.id}`);
|
||||
router.push(`/ko/accounting/bills/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleDeleteClick = useCallback((id: string) => {
|
||||
@@ -425,7 +425,7 @@ export function BillManagementClient({
|
||||
// 등록 버튼
|
||||
createButton: {
|
||||
label: '어음 등록',
|
||||
onClick: () => router.push('/ko/accounting/bills/new'),
|
||||
onClick: () => router.push('/ko/accounting/bills?mode=new'),
|
||||
icon: Plus,
|
||||
},
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: BillRecord) => {
|
||||
router.push(`/ko/accounting/bills/${item.id}`);
|
||||
router.push(`/ko/accounting/bills/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: BillRecord) => {
|
||||
@@ -177,7 +177,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
}, [router]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/accounting/bills/new');
|
||||
router.push('/ko/accounting/bills?mode=new');
|
||||
}, [router]);
|
||||
|
||||
// 저장 핸들러 (선택된 항목의 상태 일괄 변경)
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function CardTransactionDetailClient({
|
||||
const handleModeChange = useCallback(
|
||||
(newMode: DetailMode) => {
|
||||
if (newMode === 'edit' && transactionId) {
|
||||
router.push(`/ko/accounting/card-transactions/${transactionId}/edit`);
|
||||
router.push(`/ko/accounting/card-transactions/${transactionId}?mode=edit`);
|
||||
} else {
|
||||
setMode(newMode);
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ export function CardTransactionInquiry({
|
||||
|
||||
// 헤더 액션 (등록 버튼)
|
||||
headerActions: () => (
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/card-transactions/new')}>
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/card-transactions?mode=new')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
카드내역 등록
|
||||
</Button>
|
||||
|
||||
@@ -129,7 +129,7 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) {
|
||||
if (isNewMode) {
|
||||
router.push('/ko/accounting/deposits');
|
||||
} else {
|
||||
router.push(`/ko/accounting/deposits/${depositId}`);
|
||||
router.push(`/ko/accounting/deposits/${depositId}?mode=view`);
|
||||
}
|
||||
}, [router, depositId, isNewMode]);
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function DepositDetailClientV2({
|
||||
const handleModeChange = useCallback(
|
||||
(newMode: DetailMode) => {
|
||||
if (newMode === 'edit' && depositId) {
|
||||
router.push(`/ko/accounting/deposits/${depositId}/edit`);
|
||||
router.push(`/ko/accounting/deposits/${depositId}?mode=edit`);
|
||||
} else {
|
||||
setMode(newMode);
|
||||
}
|
||||
@@ -120,9 +120,17 @@ export default function DepositDetailClientV2({
|
||||
[depositId, router]
|
||||
);
|
||||
|
||||
// 타이틀 동적 설정
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "입금 상세"로 표시하려면 직접 설정 필요
|
||||
const dynamicConfig = {
|
||||
...depositDetailConfig,
|
||||
title: mode === 'view' ? '입금 상세' : '입금',
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={depositDetailConfig as Parameters<typeof IntegratedDetailTemplate>[0]['config']}
|
||||
config={dynamicConfig as Parameters<typeof IntegratedDetailTemplate>[0]['config']}
|
||||
mode={mode}
|
||||
initialData={deposit as unknown as Record<string, unknown> | undefined}
|
||||
itemId={depositId}
|
||||
|
||||
@@ -144,7 +144,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: DepositRecord) => {
|
||||
router.push(`/ko/accounting/deposits/${item.id}`);
|
||||
router.push(`/ko/accounting/deposits/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: DepositRecord) => {
|
||||
|
||||
@@ -196,7 +196,7 @@ export function PurchaseManagement() {
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: PurchaseRecord) => {
|
||||
router.push(`/ko/accounting/purchase/${item.id}`);
|
||||
router.push(`/ko/accounting/purchase/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: PurchaseRecord) => {
|
||||
|
||||
@@ -555,9 +555,11 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
const templateMode = isNewMode ? 'create' : mode;
|
||||
|
||||
// ===== 동적 config =====
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "매출 상세"로 표시하려면 직접 설정 필요
|
||||
const dynamicConfig = {
|
||||
...salesConfig,
|
||||
title: isNewMode ? '매출' : '매출 상세',
|
||||
title: isViewMode ? '매출 상세' : '매출',
|
||||
actions: {
|
||||
...salesConfig.actions,
|
||||
submitLabel: isNewMode ? '등록' : '저장',
|
||||
|
||||
@@ -180,7 +180,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: SalesRecord) => {
|
||||
router.push(`/ko/accounting/sales/${item.id}`);
|
||||
router.push(`/ko/accounting/sales/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: SalesRecord) => {
|
||||
|
||||
@@ -112,7 +112,7 @@ export function VendorLedgerDetail({
|
||||
const handleEditTransaction = useCallback((entry: TransactionEntry) => {
|
||||
// 어음 관련 항목일 경우 어음 상세로 이동
|
||||
if (entry.type === 'note' && entry.noteInfo) {
|
||||
router.push(`/ko/accounting/bills/${entry.id}`);
|
||||
router.push(`/ko/accounting/bills/${entry.id}?mode=view`);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
|
||||
@@ -646,9 +646,11 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
);
|
||||
|
||||
// config 동적 수정 (등록 모드일 때 타이틀 변경)
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "거래처 상세"로 표시하려면 직접 설정 필요
|
||||
const dynamicConfig = {
|
||||
...vendorConfig,
|
||||
title: isNewMode ? '거래처 등록' : '거래처 상세',
|
||||
title: isViewMode ? '거래처 상세' : '거래처',
|
||||
description: isNewMode ? '새로운 거래처를 등록합니다' : '거래처 상세 정보 및 신용등급을 관리합니다',
|
||||
actions: {
|
||||
...vendorConfig.actions,
|
||||
|
||||
@@ -549,9 +549,11 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
);
|
||||
|
||||
// config 동적 수정 (등록 모드일 때 타이틀 변경)
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "거래처 상세"로 표시하려면 직접 설정 필요
|
||||
const dynamicConfig = {
|
||||
...vendorConfig,
|
||||
title: isNewMode ? '거래처 등록' : '거래처',
|
||||
title: isViewMode ? '거래처 상세' : '거래처',
|
||||
description: isNewMode ? '새로운 거래처를 등록합니다' : '거래처 상세 정보 및 신용등급을 관리합니다',
|
||||
actions: {
|
||||
...vendorConfig.actions,
|
||||
|
||||
@@ -171,7 +171,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
|
||||
|
||||
// ===== 액션 핸들러 =====
|
||||
const handleRowClick = useCallback((item: Vendor) => {
|
||||
router.push(`/ko/accounting/vendors/${item.id}`);
|
||||
router.push(`/ko/accounting/vendors/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: Vendor) => {
|
||||
|
||||
@@ -88,7 +88,7 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(vendor: Vendor) => {
|
||||
router.push(`/ko/accounting/vendors/${vendor.id}`);
|
||||
router.push(`/ko/accounting/vendors/${vendor.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -129,7 +129,7 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps)
|
||||
if (isNewMode) {
|
||||
router.push('/ko/accounting/withdrawals');
|
||||
} else {
|
||||
router.push(`/ko/accounting/withdrawals/${withdrawalId}`);
|
||||
router.push(`/ko/accounting/withdrawals/${withdrawalId}?mode=view`);
|
||||
}
|
||||
}, [router, withdrawalId, isNewMode]);
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function WithdrawalDetailClientV2({
|
||||
const handleModeChange = useCallback(
|
||||
(newMode: DetailMode) => {
|
||||
if (newMode === 'edit' && withdrawalId) {
|
||||
router.push(`/ko/accounting/withdrawals/${withdrawalId}/edit`);
|
||||
router.push(`/ko/accounting/withdrawals/${withdrawalId}?mode=edit`);
|
||||
} else {
|
||||
setMode(newMode);
|
||||
}
|
||||
@@ -120,9 +120,17 @@ export default function WithdrawalDetailClientV2({
|
||||
[withdrawalId, router]
|
||||
);
|
||||
|
||||
// 타이틀 동적 설정
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "출금 상세"로 표시하려면 직접 설정 필요
|
||||
const dynamicConfig = {
|
||||
...withdrawalDetailConfig,
|
||||
title: mode === 'view' ? '출금 상세' : '출금',
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={withdrawalDetailConfig as Parameters<typeof IntegratedDetailTemplate>[0]['config']}
|
||||
config={dynamicConfig as Parameters<typeof IntegratedDetailTemplate>[0]['config']}
|
||||
mode={mode}
|
||||
initialData={withdrawal as unknown as Record<string, unknown> | undefined}
|
||||
itemId={withdrawalId}
|
||||
|
||||
@@ -160,7 +160,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: WithdrawalRecord) => {
|
||||
router.push(`/ko/accounting/withdrawals/${item.id}`);
|
||||
router.push(`/ko/accounting/withdrawals/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback((item: WithdrawalRecord) => {
|
||||
|
||||
@@ -235,7 +235,7 @@ export function DraftBox() {
|
||||
);
|
||||
|
||||
const handleNewDocument = useCallback(() => {
|
||||
router.push('/ko/approval/draft/new');
|
||||
router.push('/ko/approval/draft?mode=new');
|
||||
}, [router]);
|
||||
|
||||
const handleSendNotification = useCallback(async () => {
|
||||
|
||||
@@ -55,7 +55,7 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/board/${post.boardCode}/${post.id}/edit`);
|
||||
router.push(`/ko/board/${post.boardCode}/${post.id}?mode=edit`);
|
||||
}, [router, post.boardCode, post.id]);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { DetailConfig } from '@/components/templates/IntegratedDetailTempla
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
export const boardCreateConfig: DetailConfig = {
|
||||
title: '게시글 등록',
|
||||
title: '게시글',
|
||||
description: '새로운 게시글을 등록합니다',
|
||||
icon: FileText,
|
||||
basePath: '/boards',
|
||||
|
||||
@@ -169,9 +169,9 @@ export function BoardListUnified() {
|
||||
onClick={() => {
|
||||
const boardCode = activeTab !== 'my' ? activeTab : boards[0]?.boardCode;
|
||||
if (boardCode) {
|
||||
router.push(`/ko/board/${boardCode}/create`);
|
||||
router.push(`/ko/board/${boardCode}?mode=new`);
|
||||
} else {
|
||||
router.push('/ko/board/create');
|
||||
router.push('/ko/board?mode=new');
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -198,11 +198,11 @@ export function BoardListUnified() {
|
||||
const isMyPost = item.authorId === currentUserId;
|
||||
|
||||
const handleRowClick = () => {
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}`);
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}?mode=view`);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}/edit`);
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}?mode=edit`);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -289,11 +289,11 @@ export function BoardListUnified() {
|
||||
const isMyPost = item.authorId === currentUserId;
|
||||
|
||||
const handleRowClick = () => {
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}`);
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}?mode=view`);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}/edit`);
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}?mode=edit`);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
|
||||
@@ -130,7 +130,7 @@ export function BoardList() {
|
||||
// ===== 액션 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Post) => {
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}`);
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
@@ -138,9 +138,9 @@ export function BoardList() {
|
||||
const handleNewPost = useCallback(() => {
|
||||
const boardCode = activeTab !== 'my' ? activeTab : boards[0]?.boardCode;
|
||||
if (boardCode) {
|
||||
router.push(`/ko/board/${boardCode}/create`);
|
||||
router.push(`/ko/board/${boardCode}?mode=new`);
|
||||
} else {
|
||||
router.push('/ko/board/create');
|
||||
router.push('/ko/board?mode=new');
|
||||
}
|
||||
}, [router, activeTab, boards]);
|
||||
|
||||
@@ -293,7 +293,7 @@ export function BoardList() {
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-600 hover:text-gray-700 hover:bg-gray-50"
|
||||
onClick={() =>
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}/edit`)
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}?mode=edit`)
|
||||
}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
@@ -352,7 +352,7 @@ export function BoardList() {
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() =>
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}/edit`)
|
||||
router.push(`/ko/board/${item.boardCode}/${item.id}?mode=edit`)
|
||||
}
|
||||
>
|
||||
<Pencil className="w-4 h-4 mr-2" /> 수정
|
||||
|
||||
@@ -49,8 +49,8 @@ export function BoardDetail({ board, onEdit, onDelete }: BoardDetailProps) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="게시판관리 상세"
|
||||
description="게시판 목록을 관리합니다"
|
||||
title="게시판 상세"
|
||||
description="게시판 정보를 조회합니다"
|
||||
icon={ClipboardList}
|
||||
/>
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
|
||||
if (result.success) {
|
||||
await forceRefreshMenus();
|
||||
toast.success('게시판이 수정되었습니다.');
|
||||
router.push(`${BASE_PATH}/${boardData.id}`);
|
||||
router.push(`${BASE_PATH}/${boardData.id}?mode=view`);
|
||||
} else {
|
||||
setError(result.error || '게시판 수정에 실패했습니다.');
|
||||
toast.error(result.error || '게시판 수정에 실패했습니다.');
|
||||
|
||||
@@ -85,7 +85,7 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
|
||||
|
||||
const handleBack = () => {
|
||||
if (mode === 'edit' && board) {
|
||||
router.push(`/ko/board/board-management/${board.id}`);
|
||||
router.push(`/ko/board/board-management/${board.id}?mode=view`);
|
||||
} else {
|
||||
router.push('/ko/board/board-management');
|
||||
}
|
||||
@@ -124,8 +124,8 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title={mode === 'create' ? '게시판관리 상세' : '게시판관리 상세'}
|
||||
description="게시판 목록을 관리합니다"
|
||||
title={mode === 'create' ? '게시판 등록' : '게시판 수정'}
|
||||
description={mode === 'create' ? '새 게시판을 등록합니다' : '게시판 정보를 수정합니다'}
|
||||
icon={ClipboardList}
|
||||
/>
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ const createBoardManagementConfig = (router: ReturnType<typeof useRouter>): Univ
|
||||
createButton: {
|
||||
label: '게시판 등록',
|
||||
icon: Plus,
|
||||
onClick: () => router.push('/board/board-management/new'),
|
||||
onClick: () => router.push('/board/board-management?mode=new'),
|
||||
},
|
||||
|
||||
// 삭제 확인 메시지
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function BiddingDetailForm({
|
||||
const result = await updateBidding(biddingId, formData);
|
||||
if (result.success) {
|
||||
toast.success('수정이 완료되었습니다.');
|
||||
router.push(`/ko/construction/project/bidding/${biddingId}`);
|
||||
router.push(`/ko/construction/project/bidding/${biddingId}?mode=view`);
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
} else {
|
||||
@@ -524,9 +524,10 @@ export default function BiddingDetailForm({
|
||||
);
|
||||
|
||||
// 템플릿 동적 설정
|
||||
// Note: IntegratedDetailTemplate이 모드에 따라 '상세'/'수정' 자동 추가
|
||||
const dynamicConfig = {
|
||||
...biddingConfig,
|
||||
title: isViewMode ? '입찰 상세' : '입찰 수정',
|
||||
title: '입찰',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -114,14 +114,14 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Bidding) => {
|
||||
router.push(`/ko/construction/project/bidding/${item.id}`);
|
||||
router.push(`/ko/construction/project/bidding/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Bidding) => {
|
||||
router.push(`/ko/construction/project/bidding/${item.id}/edit`);
|
||||
router.push(`/ko/construction/project/bidding/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function ContractDetailForm({
|
||||
const result = await createContract(formData);
|
||||
if (result.success && result.data) {
|
||||
toast.success(isChangeContract ? '변경 계약서가 생성되었습니다.' : '계약이 생성되었습니다.');
|
||||
router.push(`/ko/construction/project/contract/${result.data.id}`);
|
||||
router.push(`/ko/construction/project/contract/${result.data.id}?mode=view`);
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export default function ContractDetailForm({
|
||||
const result = await updateContract(contractId, formData);
|
||||
if (result.success) {
|
||||
toast.success('수정이 완료되었습니다.');
|
||||
router.push(`/ko/construction/project/contract/${contractId}`);
|
||||
router.push(`/ko/construction/project/contract/${contractId}?mode=view`);
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
}
|
||||
@@ -212,11 +212,12 @@ export default function ContractDetailForm({
|
||||
}, []);
|
||||
|
||||
// 모드별 config 타이틀 동적 설정
|
||||
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'상세' 자동 추가
|
||||
const dynamicConfig = useMemo(() => {
|
||||
if (isCreateMode) {
|
||||
return {
|
||||
...contractConfig,
|
||||
title: isChangeContract ? '변경 계약서 생성' : '계약 등록',
|
||||
title: isChangeContract ? '변경 계약서' : '계약',
|
||||
actions: {
|
||||
...contractConfig.actions,
|
||||
showDelete: false, // create 모드에서는 삭제 버튼 없음
|
||||
@@ -512,7 +513,7 @@ export default function ContractDetailForm({
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={mode === 'create' ? 'new' : mode}
|
||||
mode={mode}
|
||||
initialData={{}}
|
||||
itemId={contractId}
|
||||
isLoading={false}
|
||||
|
||||
@@ -122,14 +122,14 @@ export default function ContractListClient({ initialData = [], initialStats }: C
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Contract) => {
|
||||
router.push(`/ko/construction/project/contract/${item.id}`);
|
||||
router.push(`/ko/construction/project/contract/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Contract) => {
|
||||
router.push(`/ko/construction/project/contract/${item.id}/edit`);
|
||||
router.push(`/ko/construction/project/contract/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function EstimateDetailForm({
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}/edit`);
|
||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}?mode=edit`);
|
||||
}, [router, estimateId]);
|
||||
|
||||
// ===== 저장/삭제 핸들러 =====
|
||||
@@ -123,7 +123,7 @@ export default function EstimateDetailForm({
|
||||
if (result.success) {
|
||||
toast.success('수정이 완료되었습니다.');
|
||||
setShowSaveDialog(false);
|
||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}`);
|
||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}?mode=view`);
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
@@ -191,7 +191,7 @@ export default function EstimateDetailForm({
|
||||
toast.success('입찰이 등록되었습니다.');
|
||||
setShowBiddingDialog(false);
|
||||
// 입찰 상세 페이지로 이동
|
||||
router.push(`/ko/construction/project/bidding/${result.data.id}`);
|
||||
router.push(`/ko/construction/project/bidding/${result.data.id}?mode=view`);
|
||||
} else {
|
||||
toast.error(result.error || '입찰 등록에 실패했습니다.');
|
||||
}
|
||||
@@ -668,11 +668,12 @@ export default function EstimateDetailForm({
|
||||
]);
|
||||
|
||||
// Edit 모드용 config (타이틀 변경)
|
||||
// Note: IntegratedDetailTemplate이 모드에 따라 '상세'/'수정' 자동 추가
|
||||
const currentConfig = useMemo(() => {
|
||||
if (isEditMode) {
|
||||
return {
|
||||
...estimateConfig,
|
||||
title: '견적 수정',
|
||||
title: '견적',
|
||||
description: '견적 정보를 수정합니다',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,14 +104,14 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Estimate) => {
|
||||
router.push(`/ko/construction/project/bidding/estimates/${item.id}`);
|
||||
router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Estimate) => {
|
||||
router.push(`/ko/construction/project/bidding/estimates/${item.id}/edit`);
|
||||
router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function EstimateDocumentModal({
|
||||
const handleEdit = useCallback(() => {
|
||||
if (estimateId) {
|
||||
onClose();
|
||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}/edit`);
|
||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}?mode=edit`);
|
||||
}
|
||||
}, [estimateId, onClose, router]);
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function HandoverReportDetailForm({
|
||||
const result = await updateHandoverReport(reportId, formData);
|
||||
if (result.success) {
|
||||
toast.success('수정이 완료되었습니다.');
|
||||
router.push(`/ko/construction/project/contract/handover-report/${reportId}`);
|
||||
router.push(`/ko/construction/project/contract/handover-report/${reportId}?mode=view`);
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -129,14 +129,14 @@ export default function HandoverReportListClient({
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(report: HandoverReport) => {
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}`);
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(report: HandoverReport) => {
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}/edit`);
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ export function HandoverReportDocumentModal({
|
||||
// 수정
|
||||
const handleEdit = () => {
|
||||
onOpenChange(false);
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}/edit`);
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=edit`);
|
||||
};
|
||||
|
||||
// 삭제
|
||||
|
||||
@@ -604,10 +604,11 @@ export default function IssueDetailForm({ issue, mode = 'view' }: IssueDetailFor
|
||||
);
|
||||
|
||||
// 템플릿 모드 및 동적 설정
|
||||
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'상세' 자동 추가
|
||||
const templateMode = isCreateMode ? 'create' : mode;
|
||||
const dynamicConfig = {
|
||||
...issueConfig,
|
||||
title: isCreateMode ? '이슈 등록' : '이슈 상세',
|
||||
title: isViewMode ? '이슈 상세' : '이슈',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -107,20 +107,20 @@ export default function IssueManagementListClient({
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Issue) => {
|
||||
router.push(`/ko/construction/project/issue-management/${item.id}`);
|
||||
router.push(`/ko/construction/project/issue-management/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Issue) => {
|
||||
router.push(`/ko/construction/project/issue-management/${item.id}/edit`);
|
||||
router.push(`/ko/construction/project/issue-management/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/project/issue-management/new');
|
||||
router.push('/ko/construction/project/issue-management?mode=new');
|
||||
}, [router]);
|
||||
|
||||
// 철회 다이얼로그 열기
|
||||
|
||||
@@ -58,8 +58,8 @@ export default function ItemDetailClient({
|
||||
}: ItemDetailClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// 모드 계산
|
||||
const mode = isNewMode ? 'new' : isEditMode ? 'edit' : 'view';
|
||||
// 모드 계산 (IntegratedDetailTemplate은 'create'를 기대)
|
||||
const mode = isNewMode ? 'create' : isEditMode ? 'edit' : 'view';
|
||||
const isViewMode = mode === 'view';
|
||||
|
||||
// 폼 데이터
|
||||
@@ -161,17 +161,16 @@ export default function ItemDetailClient({
|
||||
);
|
||||
|
||||
// 동적 config (mode에 따라 title 변경)
|
||||
// Note: IntegratedDetailTemplate 제목 로직:
|
||||
// - create 모드: ${config.title} 등록
|
||||
// - view 모드: config.title (접미사 없음)
|
||||
// - edit 모드: ${config.title} 수정
|
||||
const dynamicConfig = useMemo(() => {
|
||||
const titleMap: Record<string, string> = {
|
||||
new: '품목 등록',
|
||||
edit: '품목 수정',
|
||||
view: '품목 상세',
|
||||
};
|
||||
return {
|
||||
...itemConfig,
|
||||
title: titleMap[mode] || itemConfig.title,
|
||||
title: isViewMode ? '품목 상세' : '품목',
|
||||
};
|
||||
}, [mode]);
|
||||
}, [isViewMode]);
|
||||
|
||||
// onSubmit 핸들러 (Promise 반환)
|
||||
const handleFormSubmit = useCallback(async () => {
|
||||
@@ -187,11 +186,11 @@ export default function ItemDetailClient({
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
if (mode === 'new') {
|
||||
if (mode === 'create') {
|
||||
const result = await createItem(formData);
|
||||
if (result.success && result.data) {
|
||||
toast.success('품목이 등록되었습니다.');
|
||||
router.push(`/ko/construction/order/base-info/items/${result.data.id}`);
|
||||
router.push(`/ko/construction/order/base-info/items/${result.data.id}?mode=view`);
|
||||
return { success: true };
|
||||
} else {
|
||||
toast.error(result.error || '품목 등록에 실패했습니다.');
|
||||
@@ -201,7 +200,7 @@ export default function ItemDetailClient({
|
||||
const result = await updateItem(itemId, formData);
|
||||
if (result.success) {
|
||||
toast.success('품목이 수정되었습니다.');
|
||||
router.push(`/ko/construction/order/base-info/items/${itemId}`);
|
||||
router.push(`/ko/construction/order/base-info/items/${itemId}?mode=view`);
|
||||
return { success: true };
|
||||
} else {
|
||||
toast.error(result.error || '품목 수정에 실패했습니다.');
|
||||
|
||||
@@ -210,13 +210,13 @@ export default function ItemManagementClient({
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(item: Item) => {
|
||||
router.push(`/ko/construction/order/base-info/items/${item.id}`);
|
||||
router.push(`/ko/construction/order/base-info/items/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/base-info/items/new');
|
||||
router.push('/ko/construction/order/base-info/items?mode=new');
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function LaborDetailClient({
|
||||
const result = await createLabor(formData);
|
||||
if (result.success && result.data) {
|
||||
toast.success('노임이 등록되었습니다.');
|
||||
router.push(`/ko/construction/order/base-info/labor/${result.data.id}`);
|
||||
router.push(`/ko/construction/order/base-info/labor/${result.data.id}?mode=view`);
|
||||
} else {
|
||||
toast.error(result.error || '노임 등록에 실패했습니다.');
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function LaborManagementClient({
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(labor: Labor) => {
|
||||
router.push(`/ko/construction/order/base-info/labor/${labor.id}`);
|
||||
router.push(`/ko/construction/order/base-info/labor/${labor.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
@@ -111,7 +111,7 @@ export default function LaborManagementClient({
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/base-info/labor/new');
|
||||
router.push('/ko/construction/order/base-info/labor?mode=new');
|
||||
}, [router]);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
|
||||
@@ -582,7 +582,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
|
||||
{/* 이슈 보고 카드 */}
|
||||
<Card
|
||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
onClick={() => router.push(`/ko/construction/project/issue-management/new?orderId=${detail.orderId}`)}
|
||||
onClick={() => router.push(`/ko/construction/project/issue-management?mode=new&orderId=${detail.orderId}`)}
|
||||
>
|
||||
<CardContent className="pt-6 pb-6">
|
||||
<div className="space-y-2">
|
||||
@@ -694,10 +694,18 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
|
||||
</div>
|
||||
);
|
||||
|
||||
// 동적 config 설정
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "시공 상세"로 표시하려면 직접 설정 필요
|
||||
const dynamicConfig = {
|
||||
...constructionConfig,
|
||||
title: isViewMode ? '시공 상세' : '시공',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={constructionConfig}
|
||||
config={dynamicConfig}
|
||||
mode={mode}
|
||||
initialData={{}}
|
||||
itemId={id}
|
||||
|
||||
@@ -161,14 +161,14 @@ export default function ConstructionManagementListClient({
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: ConstructionManagement) => {
|
||||
router.push(`/ko/construction/project/construction-management/${item.id}`);
|
||||
router.push(`/ko/construction/project/construction-management/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: ConstructionManagement) => {
|
||||
router.push(`/ko/construction/project/construction-management/${item.id}/edit`);
|
||||
router.push(`/ko/construction/project/construction-management/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
@@ -184,7 +184,7 @@ export default function ConstructionManagementListClient({
|
||||
|
||||
const handleCalendarEventClick = useCallback((event: ScheduleEvent) => {
|
||||
if (event.data) {
|
||||
router.push(`/ko/construction/project/construction-management/${event.id}`);
|
||||
router.push(`/ko/construction/project/construction-management/${event.id}?mode=view`);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, Fragment } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FolderKanban, Pencil, ClipboardList, PlayCircle, CheckCircle2 } from 'lucide-react';
|
||||
import { FolderKanban, ClipboardList, PlayCircle, CheckCircle2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -240,22 +240,14 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(project: Project) => {
|
||||
router.push(`/ko/construction/project/management/${project.id}`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(e: React.MouseEvent, projectId: string) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/ko/construction/project/management/${projectId}/edit`);
|
||||
router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleGanttProjectClick = useCallback(
|
||||
(project: Project) => {
|
||||
router.push(`/ko/construction/project/management/${project.id}`);
|
||||
router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
@@ -510,13 +502,12 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
<TableHead className="w-[120px] text-right">누계 기성</TableHead>
|
||||
<TableHead className="w-[180px] text-center">프로젝트 기간</TableHead>
|
||||
<TableHead className="w-[80px] text-center">상태</TableHead>
|
||||
<TableHead className="w-[80px] text-center">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="[&_tr]:h-14 [&_tr]:min-h-[56px] [&_tr]:max-h-[56px]">
|
||||
{paginatedData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={14} className="h-24 text-center">
|
||||
<TableCell colSpan={13} className="h-24 text-center">
|
||||
검색 결과가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -553,18 +544,6 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
<TableCell className="text-center">
|
||||
{getStatusBadge(project.status, project.hasUrgentIssue)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{isSelected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={(e) => handleEdit(e, project.id)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -20,8 +20,8 @@ import { OrderDialogs } from './dialogs/OrderDialogs';
|
||||
import { OrderDocumentModal } from './modals/OrderDocumentModal';
|
||||
|
||||
interface OrderDetailFormProps {
|
||||
mode: 'view' | 'edit';
|
||||
orderId: string;
|
||||
mode: 'view' | 'edit' | 'create';
|
||||
orderId?: string;
|
||||
initialData?: OrderDetail;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function OrderDetailForm({
|
||||
const result = await updateOrder(orderId, formData);
|
||||
if (result.success) {
|
||||
toast.success('수정이 완료되었습니다.');
|
||||
router.push(`/ko/construction/order/order-management/${orderId}`);
|
||||
router.push(`/ko/construction/order/order-management/${orderId}?mode=view`);
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
}
|
||||
@@ -225,10 +225,18 @@ export default function OrderDetailForm({
|
||||
</div>
|
||||
);
|
||||
|
||||
// 동적 config 설정
|
||||
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
|
||||
// view 모드에서 "발주 상세"로 표시하려면 직접 설정 필요
|
||||
const dynamicConfig = {
|
||||
...orderConfig,
|
||||
title: isViewMode ? '발주 상세' : '발주',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={orderConfig}
|
||||
config={dynamicConfig}
|
||||
mode={mode}
|
||||
initialData={{}}
|
||||
itemId={orderId}
|
||||
|
||||
@@ -113,6 +113,13 @@ export default function OrderManagementListClient({
|
||||
const calendarEvents: ScheduleEvent[] = useMemo(() => {
|
||||
return allOrders
|
||||
.filter((order) => {
|
||||
// 유효한 날짜가 있는 항목만 달력에 표시
|
||||
// periodStart/periodEnd가 빈 문자열이면 parseISO가 Invalid Date를 반환하여
|
||||
// 모든 이벤트가 일요일(0번 컬럼)에 표시되는 버그 방지
|
||||
if (!order.periodStart || !order.periodEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 현장 필터 (달력용)
|
||||
if (calendarSiteFilters.length > 0) {
|
||||
const matchingSite = MOCK_SITES.find((s) => order.siteName.includes(s.label.split(' ')[0]));
|
||||
@@ -152,20 +159,20 @@ export default function OrderManagementListClient({
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(order: Order) => {
|
||||
router.push(`/ko/construction/order/order-management/${order.id}`);
|
||||
router.push(`/ko/construction/order/order-management/${order.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(order: Order) => {
|
||||
router.push(`/ko/construction/order/order-management/${order.id}/edit`);
|
||||
router.push(`/ko/construction/order/order-management/${order.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/order-management/new');
|
||||
router.push('/ko/construction/order/order-management?mode=new');
|
||||
}, [router]);
|
||||
|
||||
// 달력 이벤트 핸들러
|
||||
@@ -179,7 +186,7 @@ export default function OrderManagementListClient({
|
||||
|
||||
const handleCalendarEventClick = useCallback((event: ScheduleEvent) => {
|
||||
if (event.data) {
|
||||
router.push(`/ko/construction/order/order-management/${event.id}`);
|
||||
router.push(`/ko/construction/order/order-management/${event.id}?mode=view`);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
@@ -366,6 +373,10 @@ export default function OrderManagementListClient({
|
||||
|
||||
// 달력 날짜 필터
|
||||
if (selectedCalendarDate) {
|
||||
// periodStart/periodEnd가 빈 문자열이면 필터링에서 제외
|
||||
if (!item.periodStart || !item.periodEnd) {
|
||||
return false;
|
||||
}
|
||||
const orderStart = startOfDay(parseISO(item.periodStart));
|
||||
const orderEnd = startOfDay(parseISO(item.periodEnd));
|
||||
const selected = startOfDay(selectedCalendarDate);
|
||||
|
||||
@@ -155,7 +155,7 @@ export function OrderManagementUnified({ initialData }: OrderManagementUnifiedPr
|
||||
|
||||
const handleCalendarEventClick = useCallback((event: ScheduleEvent) => {
|
||||
if (event.data) {
|
||||
router.push(`/ko/construction/order/order-management/${event.id}`);
|
||||
router.push(`/ko/construction/order/order-management/${event.id}?mode=view`);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
|
||||
@@ -136,8 +136,11 @@ function transformOrder(apiOrder: ApiOrder): Order {
|
||||
plannedDeliveryDate: apiOrder.delivery_date || '',
|
||||
actualDeliveryDate: apiOrder.actual_delivery_date || null,
|
||||
status: transformStatus(apiOrder.status_code),
|
||||
periodStart: apiOrder.received_at || '',
|
||||
periodEnd: apiOrder.delivery_date || '',
|
||||
// 달력 표시용 기간: received_at ~ delivery_date
|
||||
// received_at이 없으면 delivery_date를 시작일로 사용 (단일 날짜 이벤트)
|
||||
// delivery_date도 없으면 created_at을 사용
|
||||
periodStart: apiOrder.received_at || apiOrder.delivery_date || apiOrder.created_at.split('T')[0],
|
||||
periodEnd: apiOrder.delivery_date || apiOrder.received_at || apiOrder.created_at.split('T')[0],
|
||||
createdAt: apiOrder.created_at,
|
||||
updatedAt: apiOrder.updated_at,
|
||||
};
|
||||
|
||||
@@ -158,7 +158,7 @@ export function useOrderDetailForm({
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/construction/order/order-management/${orderId}/edit`);
|
||||
router.push(`/ko/construction/order/order-management/${orderId}?mode=edit`);
|
||||
}, [router, orderId]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
@@ -239,7 +239,7 @@ export function useOrderDetailForm({
|
||||
const result = await duplicateOrder(orderId);
|
||||
if (result.success && result.newId) {
|
||||
toast.success('발주가 복제되었습니다.');
|
||||
router.push(`/ko/construction/order/order-management/${result.newId}/edit`);
|
||||
router.push(`/ko/construction/order/order-management/${result.newId}?mode=edit`);
|
||||
} else {
|
||||
toast.error(result.error || '복제에 실패했습니다.');
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function OrderDocumentModal({
|
||||
// 수정
|
||||
const handleEdit = () => {
|
||||
onOpenChange(false);
|
||||
router.push(`/ko/construction/order/order-management/${order.id}/edit`);
|
||||
router.push(`/ko/construction/order/order-management/${order.id}?mode=edit`);
|
||||
};
|
||||
|
||||
// 삭제
|
||||
|
||||
@@ -541,7 +541,9 @@ export function orderDetailToFormData(detail: OrderDetail): OrderDetailFormData
|
||||
// orderItems를 카테고리별로 그룹핑
|
||||
const categoryMap = new Map<string, OrderDetailCategory>();
|
||||
|
||||
detail.orderItems.forEach((item) => {
|
||||
// orderItems가 없거나 undefined일 경우 빈 배열로 처리
|
||||
const orderItems = detail.orderItems || [];
|
||||
orderItems.forEach((item) => {
|
||||
// 임시로 'default' 카테고리 사용 (실제로는 item에 categoryId가 있어야 함)
|
||||
const categoryId = 'default';
|
||||
const categoryName = '기본 카테고리';
|
||||
@@ -558,27 +560,27 @@ export function orderDetailToFormData(detail: OrderDetail): OrderDetailFormData
|
||||
});
|
||||
|
||||
return {
|
||||
orderNumber: detail.orderNumber,
|
||||
orderCompanyId: detail.orderCompanyId,
|
||||
orderType: detail.orderType,
|
||||
status: detail.status,
|
||||
orderManager: detail.orderManager,
|
||||
deliveryAddress: detail.deliveryAddress,
|
||||
partnerId: detail.partnerId,
|
||||
partnerName: detail.partnerName,
|
||||
siteName: detail.siteName,
|
||||
contractNumber: detail.contractNumber,
|
||||
contractId: detail.contractId,
|
||||
constructionPMId: detail.constructionPMId,
|
||||
constructionPM: detail.constructionPM,
|
||||
constructionManagers: detail.constructionManagers,
|
||||
workTeamLeader: detail.workTeamLeader,
|
||||
constructionStartDate: detail.constructionStartDate,
|
||||
orderNumber: detail.orderNumber || '',
|
||||
orderCompanyId: detail.orderCompanyId || '',
|
||||
orderType: detail.orderType || 'steel_bar',
|
||||
status: detail.status || 'waiting',
|
||||
orderManager: detail.orderManager || '',
|
||||
deliveryAddress: detail.deliveryAddress || '',
|
||||
partnerId: detail.partnerId || '',
|
||||
partnerName: detail.partnerName || '',
|
||||
siteName: detail.siteName || '',
|
||||
contractNumber: detail.contractNumber || '',
|
||||
contractId: detail.contractId || '',
|
||||
constructionPMId: detail.constructionPMId || '',
|
||||
constructionPM: detail.constructionPM || '',
|
||||
constructionManagers: detail.constructionManagers || [],
|
||||
workTeamLeader: detail.workTeamLeader || '',
|
||||
constructionStartDate: detail.constructionStartDate || '',
|
||||
constructionEndDate: '', // Order 인터페이스에는 없으므로 빈 값
|
||||
orderCategories: Array.from(categoryMap.values()),
|
||||
memo: detail.memo,
|
||||
periodStart: detail.periodStart,
|
||||
periodEnd: detail.periodEnd,
|
||||
memo: detail.memo || '',
|
||||
periodStart: detail.periodStart || '',
|
||||
periodEnd: detail.periodEnd || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -233,18 +233,19 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
}, []);
|
||||
|
||||
// 동적 Config (모드별 타이틀/설명)
|
||||
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'수정' 자동 추가
|
||||
const dynamicConfig = useMemo(() => {
|
||||
if (isNewMode) {
|
||||
return {
|
||||
...partnerConfig,
|
||||
title: '거래처 등록',
|
||||
title: '거래처',
|
||||
description: '새로운 거래처를 등록합니다',
|
||||
};
|
||||
}
|
||||
if (isEditMode) {
|
||||
return {
|
||||
...partnerConfig,
|
||||
title: '거래처 수정',
|
||||
title: '거래처',
|
||||
description: '거래처 정보를 수정합니다',
|
||||
};
|
||||
}
|
||||
@@ -628,7 +629,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={mode}
|
||||
mode={mode === 'new' ? 'create' : mode}
|
||||
initialData={{}}
|
||||
itemId={partnerId}
|
||||
isLoading={false}
|
||||
|
||||
@@ -70,18 +70,18 @@ export default function PartnerListClient({ initialData = [], initialStats }: Pa
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Partner) => {
|
||||
router.push(`/ko/construction/project/bidding/partners/${item.id}`);
|
||||
router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/project/bidding/partners/new');
|
||||
router.push('/ko/construction/project/bidding/partners?mode=new');
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Partner) => {
|
||||
router.push(`/ko/construction/project/bidding/partners/${item.id}/edit`);
|
||||
router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -173,7 +173,7 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro
|
||||
|
||||
if (result.success) {
|
||||
toast.success('단가가 수정되었습니다.');
|
||||
router.push(`/ko/construction/order/base-info/pricing/${id}`);
|
||||
router.push(`/ko/construction/order/base-info/pricing/${id}?mode=view`);
|
||||
} else {
|
||||
toast.error(result.error || '수정에 실패했습니다.');
|
||||
}
|
||||
@@ -209,7 +209,7 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro
|
||||
// 수정 페이지로 이동
|
||||
const handleEdit = useCallback(() => {
|
||||
if (id) {
|
||||
router.push(`/ko/construction/order/base-info/pricing/${id}/edit`);
|
||||
router.push(`/ko/construction/order/base-info/pricing/${id}?mode=edit`);
|
||||
}
|
||||
}, [id, router]);
|
||||
|
||||
@@ -218,7 +218,7 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro
|
||||
if (isCreateMode) {
|
||||
router.push('/ko/construction/order/base-info/pricing');
|
||||
} else if (isEditMode && id) {
|
||||
router.push(`/ko/construction/order/base-info/pricing/${id}`);
|
||||
router.push(`/ko/construction/order/base-info/pricing/${id}?mode=view`);
|
||||
}
|
||||
}, [isCreateMode, isEditMode, id, router]);
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function PricingDetailClientV2({
|
||||
(newMode: DetailMode) => {
|
||||
if (newMode === 'edit' && pricingId) {
|
||||
// edit 모드로 변경 시 별도 페이지로 이동 (기존 라우트 구조 유지)
|
||||
router.push(`/ko/construction/order/base-info/pricing/${pricingId}/edit`);
|
||||
router.push(`/ko/construction/order/base-info/pricing/${pricingId}?mode=edit`);
|
||||
} else {
|
||||
setMode(newMode);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user