feat(시공사): 2.1 현장관리 - Frontend API 연동

- actions.ts: Mock 데이터 → API 연동
- types.ts: SiteStats에 suspended, pending 추가
- 문서: API 연동 상세 문서 추가
This commit is contained in:
2026-01-09 16:35:12 +09:00
parent 78e193c8df
commit 273d5709cd
3 changed files with 308 additions and 135 deletions

View File

@@ -1,195 +1,276 @@
'use server';
import type { Site, SiteStats } from './types';
import { cookies } from 'next/headers';
import type { Site, SiteStats, SiteStatus } from './types';
// 목업 현장 데이터
const MOCK_SITES: Site[] = [
{
id: '1',
siteCode: '123123',
partnerId: '1',
partnerName: '회사명',
siteName: '현장명',
address: '-',
status: 'unregistered',
createdAt: '2025-09-01T00:00:00Z',
updatedAt: '2025-09-01T00:00:00Z',
},
{
id: '2',
siteCode: '123123',
partnerId: '1',
partnerName: '회사명',
siteName: '현장명',
address: '서울시 강남구 대현빌라 123길',
status: 'suspended',
createdAt: '2025-09-02T00:00:00Z',
updatedAt: '2025-09-02T00:00:00Z',
},
{
id: '3',
siteCode: '123123',
partnerId: '2',
partnerName: '회사명',
siteName: '현장명',
address: '서울시 강남구 대현빌라 123길',
status: 'active',
createdAt: '2025-09-03T00:00:00Z',
updatedAt: '2025-09-03T00:00:00Z',
},
{
id: '4',
siteCode: '123123',
partnerId: '1',
partnerName: '회사명',
siteName: '현장명',
address: '서울시 강남구 대현빌라 123길',
status: 'active',
createdAt: '2025-09-04T00:00:00Z',
updatedAt: '2025-09-04T00:00:00Z',
},
{
id: '5',
siteCode: '123123',
partnerId: '3',
partnerName: '회사명',
siteName: '현장명',
address: '서울시 강남구 대현빌라 123길',
status: 'active',
createdAt: '2025-09-05T00:00:00Z',
updatedAt: '2025-09-05T00:00:00Z',
},
{
id: '6',
siteCode: '123123',
partnerId: '1',
partnerName: '회사명',
siteName: '현장명',
address: '서울시 강남구 대현빌라 123길',
status: 'active',
createdAt: '2025-09-06T00:00:00Z',
updatedAt: '2025-09-06T00:00:00Z',
},
{
id: '7',
siteCode: '123123',
partnerId: '2',
partnerName: '회사명',
siteName: '현장명',
address: '서울시 강남구 대현빌라 123길',
status: 'pending',
createdAt: '2025-09-07T00:00:00Z',
updatedAt: '2025-09-07T00:00:00Z',
},
];
/**
* 주일 기업 - 현장관리 Server Actions
* API 연동 버전
*/
// API 기본 URL
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://api.sam.kr';
const API_KEY = process.env.API_KEY || '';
/**
* API 요청 헬퍼 함수
*/
async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ success: boolean; data?: T; error?: string; message?: string }> {
try {
const cookieStore = await cookies();
const accessToken = cookieStore.get('access_token')?.value;
const headers: Record<string, string> = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-API-KEY': API_KEY,
};
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
const url = `${API_BASE_URL}/api/v1${endpoint}`;
console.log('🔵 [Site API]', options.method || 'GET', url);
const response = await fetch(url, {
...options,
headers: {
...headers,
...options.headers,
},
});
const result = await response.json();
console.log('🔵 [Site API] Response status:', response.status);
if (!response.ok) {
return {
success: false,
error: result.message || `API 오류: ${response.status}`,
};
}
return {
success: result.success ?? true,
data: result.data,
message: result.message,
};
} catch (error) {
console.error('API request error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
};
}
}
/**
* API 응답 → 프론트엔드 타입 변환
*/
function transformSite(apiData: Record<string, unknown>): Site {
// client 관계 데이터 추출
const client = apiData.client as Record<string, unknown> | null | undefined;
return {
id: String(apiData.id),
siteCode: String(apiData.site_code || ''),
partnerId: apiData.client_id ? String(apiData.client_id) : '',
partnerName: client ? String(client.name || '') : '',
siteName: String(apiData.name || ''),
address: String(apiData.address || ''),
status: (apiData.status as SiteStatus) || 'unregistered',
createdAt: String(apiData.created_at || ''),
updatedAt: String(apiData.updated_at || ''),
};
}
// ============================================================
// API 연동 함수
// ============================================================
interface GetSiteListParams {
size?: number;
page?: number;
startDate?: string;
endDate?: string;
search?: string;
status?: string;
clientId?: string;
sortBy?: string;
}
interface GetSiteListResult {
success: boolean;
data?: {
items: Site[];
totalCount: number;
total: number;
page: number;
size: number;
totalPages: number;
};
error?: string;
}
// 현장 목록 조회
/**
* 현장 목록 조회
*/
export async function getSiteList(params: GetSiteListParams = {}): Promise<GetSiteListResult> {
try {
// TODO: API 연동 시 실제 API 호출로 변경
await new Promise((resolve) => setTimeout(resolve, 500));
const queryParams = new URLSearchParams();
let filteredSites = [...MOCK_SITES];
if (params.search) queryParams.append('search', params.search);
if (params.status && params.status !== 'all') queryParams.append('status', params.status);
if (params.clientId && params.clientId !== 'all') queryParams.append('client_id', params.clientId);
if (params.startDate) queryParams.append('start_date', params.startDate);
if (params.endDate) queryParams.append('end_date', params.endDate);
if (params.page) queryParams.append('page', String(params.page));
if (params.size) queryParams.append('per_page', String(params.size));
// 날짜 필터
if (params.startDate) {
filteredSites = filteredSites.filter(
(site) => new Date(site.createdAt) >= new Date(params.startDate!)
);
// 정렬 파라미터 변환
if (params.sortBy) {
const sortMap: Record<string, { field: string; dir: string }> = {
latest: { field: 'created_at', dir: 'desc' },
oldest: { field: 'created_at', dir: 'asc' },
partnerNameAsc: { field: 'client_id', dir: 'asc' },
partnerNameDesc: { field: 'client_id', dir: 'desc' },
siteNameAsc: { field: 'name', dir: 'asc' },
siteNameDesc: { field: 'name', dir: 'desc' },
};
const sort = sortMap[params.sortBy];
if (sort) {
queryParams.append('sort_by', sort.field);
queryParams.append('sort_dir', sort.dir);
}
}
if (params.endDate) {
filteredSites = filteredSites.filter(
(site) => new Date(site.createdAt) <= new Date(params.endDate!)
);
const queryString = queryParams.toString();
const endpoint = `/sites${queryString ? `?${queryString}` : ''}`;
const result = await apiRequest<{
data: Record<string, unknown>[];
current_page: number;
per_page: number;
total: number;
last_page: number;
}>(endpoint);
if (!result.success || !result.data) {
return { success: false, error: result.error || '현장 목록 조회에 실패했습니다.' };
}
const apiData = result.data;
const items = (apiData.data || []).map(transformSite);
return {
success: true,
data: {
items: filteredSites,
totalCount: filteredSites.length,
items,
total: apiData.total || 0,
page: apiData.current_page || 1,
size: apiData.per_page || 20,
totalPages: apiData.last_page || 1,
},
};
} catch (error) {
console.error('getSiteList error:', error);
return {
success: false,
error: '현장 목록을 불러오는데 실패했습니다.',
};
return { success: false, error: '현장 목록을 불러오는데 실패했습니다.' };
}
}
// 현장 통계 조회
export async function getSiteStats(): Promise<{ success: boolean; data?: SiteStats; error?: string }> {
try {
// TODO: API 연동 시 실제 API 호출로 변경
await new Promise((resolve) => setTimeout(resolve, 300));
interface GetSiteStatsResult {
success: boolean;
data?: SiteStats;
error?: string;
}
const total = MOCK_SITES.length;
const construction = MOCK_SITES.filter((s) => s.status === 'active').length;
const unregistered = MOCK_SITES.filter((s) => s.status === 'unregistered').length;
/**
* 현장 통계 조회
*/
export async function getSiteStats(): Promise<GetSiteStatsResult> {
try {
const result = await apiRequest<{
total: number;
construction: number;
unregistered: number;
suspended: number;
pending: number;
}>('/sites/stats');
if (!result.success || !result.data) {
return { success: false, error: result.error || '현장 통계 조회에 실패했습니다.' };
}
return {
success: true,
data: {
total,
construction,
unregistered,
total: result.data.total || 0,
construction: result.data.construction || 0,
unregistered: result.data.unregistered || 0,
suspended: result.data.suspended || 0,
pending: result.data.pending || 0,
},
};
} catch (error) {
console.error('getSiteStats error:', error);
return {
success: false,
error: '현장 통계를 불러오는데 실패했습니다.',
};
return { success: false, error: '현장 통계를 불러오는데 실패했습니다.' };
}
}
// 현장 삭제
export async function deleteSite(id: string): Promise<{ success: boolean; error?: string }> {
interface DeleteSiteResult {
success: boolean;
error?: string;
}
/**
* 현장 삭제
*/
export async function deleteSite(id: string): Promise<DeleteSiteResult> {
try {
// TODO: API 연동 시 실제 API 호출로 변경
await new Promise((resolve) => setTimeout(resolve, 500));
const result = await apiRequest(`/sites/${id}`, {
method: 'DELETE',
});
if (!result.success) {
return { success: false, error: result.error || '현장 삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
console.error('deleteSite error:', error);
return {
success: false,
error: '현장 삭제에 실패했습니다.',
};
return { success: false, error: '현장 삭제에 실패했습니다.' };
}
}
// 현장 일괄 삭제
export async function deleteSites(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
interface DeleteSitesResult {
success: boolean;
deletedCount?: number;
error?: string;
}
/**
* 현장 일괄 삭제
*/
export async function deleteSites(ids: string[]): Promise<DeleteSitesResult> {
try {
// TODO: API 연동 시 실제 API 호출로 변경
await new Promise((resolve) => setTimeout(resolve, 500));
const result = await apiRequest<{ deleted_count: number }>('/sites/bulk', {
method: 'DELETE',
body: JSON.stringify({ ids: ids.map((id) => Number(id)) }),
});
if (!result.success) {
return { success: false, error: result.error || '현장 일괄 삭제에 실패했습니다.' };
}
return {
success: true,
deletedCount: ids.length,
deletedCount: result.data?.deleted_count || ids.length,
};
} catch (error) {
console.error('deleteSites error:', error);
return {
success: false,
error: '현장 일괄 삭제에 실패했습니다.',
};
return { success: false, error: '현장 일괄 삭제에 실패했습니다.' };
}
}
}

View File

@@ -17,8 +17,10 @@ export type SiteStatus = 'unregistered' | 'suspended' | 'active' | 'pending';
// 현장 통계
export interface SiteStats {
total: number; // 전체 현장
construction: number; // 시공 현장
construction: number; // 시공 현장 (active)
unregistered: number; // 미등록 현장
suspended: number; // 중지 현장
pending: number; // 보류 현장
}
// 상태 옵션