- {renderField('현장명', 'projectName', formData.projectName, {
- required: true,
- placeholder: '현장명',
- })}
+ {/* 현장명 - 거래처 연동 Combobox */}
+
+
+
+
{
+ setSiteInputValue(e.target.value);
+ handleChange('projectName', e.target.value);
+ setShowSiteDropdown(true);
+ }}
+ onFocus={() => formData.partnerId && setShowSiteDropdown(true)}
+ onBlur={() => setTimeout(() => setShowSiteDropdown(false), 200)}
+ placeholder={
+ !formData.partnerId
+ ? '거래처를 먼저 선택해주세요'
+ : isLoadingSites
+ ? '현장 목록 로딩 중...'
+ : '현장명 입력 또는 선택'
+ }
+ disabled={isViewMode || !formData.partnerId}
+ className="bg-white pr-10"
+ />
+ {!isViewMode && formData.partnerId && (
+
+ )}
+ {/* 현장 드롭다운 */}
+ {showSiteDropdown && sites.length > 0 && (
+
+ {sites
+ .filter((site) =>
+ site.siteName.toLowerCase().includes(siteInputValue.toLowerCase())
+ )
+ .map((site) => (
+
+ ))}
+ {sites.filter((site) =>
+ site.siteName.toLowerCase().includes(siteInputValue.toLowerCase())
+ ).length === 0 && (
+
+ 일치하는 현장이 없습니다. 신규 등록하려면 + 버튼을 클릭하세요.
+
+ )}
+
+ )}
+
+ {!formData.partnerId && !isViewMode && (
+
거래처를 먼저 선택하면 해당 거래처의 현장 목록이 표시됩니다.
+ )}
+
{renderField('입찰일자', 'bidDate', formData.bidDate, {
type: 'date',
})}
@@ -736,6 +1141,53 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site
+
+ {/* 현장 신규 등록 다이얼로그 */}
+
+
+
+ 현장 신규 등록
+
+ 선택한 거래처에 새로운 현장을 등록합니다.
+
+ 등록된 현장은 현장관리 목록에도 추가됩니다.
+
+
+
+
+ 취소
+
+ {isCreatingSite && }
+ 등록
+
+
+
+
);
}
\ No newline at end of file
diff --git a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx
index 37fddaca..5f01d896 100644
--- a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx
+++ b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx
@@ -130,12 +130,11 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
if (listResult.success && listResult.data) {
setBriefings(listResult.data.items);
- // 목업 통계 계산 (참석 상태 기준)
+ // 통계 계산 (참석 상태 기준)
const items = listResult.data.items;
const total = items.length;
- // 목업: scheduled 상태는 참석예정, 나머지는 참석완료로 처리
- const scheduled = items.filter((b) => b.status === 'scheduled').length;
- const attended = items.filter((b) => b.status !== 'scheduled').length;
+ const scheduled = items.filter((b) => b.attendanceStatus === 'scheduled').length;
+ const attended = items.filter((b) => b.attendanceStatus === 'attended').length;
setStatsData({ total, scheduled, attended });
}
} catch {
@@ -155,9 +154,9 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
// 필터링된 데이터
const filteredBriefings = useMemo(() => {
return briefings.filter((briefing) => {
- // Stats 탭 필터
- if (activeStatTab === 'scheduled' && briefing.status !== 'scheduled') return false;
- if (activeStatTab === 'attended' && briefing.status === 'scheduled') return false;
+ // Stats 탭 필터 (참석 상태 기준)
+ if (activeStatTab === 'scheduled' && briefing.attendanceStatus !== 'scheduled') return false;
+ if (activeStatTab === 'attended' && briefing.attendanceStatus !== 'attended') return false;
// 거래처 필터 (다중선택 - 빈 배열 = 전체)
if (partnerFilters.length > 0) {
@@ -174,8 +173,8 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
if (!attendeeFilters.includes(attendeeId)) return false;
}
- // 상태 필터
- if (statusFilter !== 'all' && briefing.status !== statusFilter) return false;
+ // 상태 필터 (참석 상태 기준)
+ if (statusFilter !== 'all' && briefing.attendanceStatus !== statusFilter) return false;
// 검색 필터
if (searchValue) {
@@ -431,8 +430,8 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
const renderTableRow = useCallback(
(briefing: SiteBriefing, index: number, globalIndex: number) => {
const isSelected = selectedItems.has(briefing.id);
- // 목업 데이터에서 상태 매핑
- const displayStatus = briefing.status === 'scheduled' ? 'scheduled' : 'attended';
+ // 참석 상태 표시
+ const displayStatus = briefing.attendanceStatus || 'scheduled';
return (
void) => {
- const displayStatus = briefing.status === 'scheduled' ? 'scheduled' : 'attended';
+ const displayStatus = briefing.attendanceStatus || 'scheduled';
return (
| null; // 백엔드는 attendees (복수형), array 타입
+ attendance_status: string | null;
+ project_name: string | null;
+ site_count: number | null;
+ construction_start_date: string | null;
+ construction_end_date: string | null;
+ vat_type: string | null;
+ work_report: string | null;
+ attendee_count: number | null;
+ created_at: string;
+ updated_at: string;
+ created_by: string | null;
+}
+
+interface ApiSiteBriefingStats {
+ total: number;
+ scheduled: number;
+ ongoing: number;
+ completed: number;
+ cancelled: number;
+}
+
+// ========================================
+// 타입 변환 함수
+// ========================================
+
+/**
+ * API 응답 → SiteBriefing 타입 변환
+ */
+function transformSiteBriefing(apiData: ApiSiteBriefing): SiteBriefing {
+ // attendees를 JSON 문자열로 변환 (types.ts의 parseAttendeeItems에서 파싱)
+ const attendeeJson = apiData.attendees ? JSON.stringify(apiData.attendees) : '';
+
+ return {
+ id: String(apiData.id),
+ briefingCode: apiData.briefing_code || '',
+ title: apiData.title || apiData.project_name || '',
+ description: apiData.description || '',
+ partnerId: apiData.partner_id ? String(apiData.partner_id) : '',
+ partnerName: apiData.partner_name || '',
+ briefingDate: apiData.briefing_date || '',
+ briefingTime: apiData.briefing_time || '',
+ location: apiData.location || '',
+ address: apiData.address || '',
+ status: (apiData.status as SiteBriefing['status']) || 'scheduled',
+ bidStatus: (apiData.bid_status as SiteBriefing['bidStatus']) || 'pending',
+ bidDate: apiData.bid_date,
+ attendee: attendeeJson, // JSON 문자열로 저장 (parseAttendeeItems에서 파싱)
attendees: [],
- attendeeCount: 5,
- createdAt: '2025-01-01',
- updatedAt: '2025-01-01',
- createdBy: '홍길동',
- },
- {
- id: '2',
- briefingCode: 'SB-002',
- title: '서초 아파트 리모델링',
- description: '서초구 반포동 아파트 리모델링 현장설명회',
- partnerId: '2',
- partnerName: '삼성시공',
- briefingDate: '2025-05-12',
- briefingTime: '10:00',
- location: '서초구청 소회의실',
- address: '서울특별시 서초구 남부순환로 2584',
- status: 'ongoing',
- bidStatus: 'bidding',
- bidDate: '2025-05-18',
- attendees: [],
- attendeeCount: 8,
- createdAt: '2025-01-02',
- updatedAt: '2025-01-02',
- createdBy: '김철수',
- },
- {
- id: '3',
- briefingCode: 'SB-003',
- title: '여의도 상업시설 신축',
- description: '영등포구 여의도동 상업시설 신축 현장설명회',
- partnerId: '3',
- partnerName: 'LG건설',
- briefingDate: '2025-05-13',
- briefingTime: '15:00',
- location: 'LG트윈타워 회의실',
- address: '서울특별시 영등포구 여의대로 128',
- status: 'completed',
- bidStatus: 'awarded',
- bidDate: '2025-05-20',
- attendees: [],
- attendeeCount: 12,
- createdAt: '2025-01-03',
- updatedAt: '2025-01-03',
- createdBy: '박영수',
- },
- {
- id: '4',
- briefingCode: 'SB-004',
- title: '송파 주상복합 공사',
- description: '송파구 잠실동 주상복합 건축 현장설명회',
- partnerId: '1',
- partnerName: '대한건설',
- briefingDate: '2025-05-14',
- briefingTime: '11:00',
- location: '롯데월드타워 회의실',
- address: '서울특별시 송파구 올림픽로 300',
- status: 'cancelled',
- bidStatus: 'failed',
- bidDate: null,
- attendees: [],
- attendeeCount: 0,
- createdAt: '2025-01-04',
- updatedAt: '2025-01-04',
- createdBy: '최민수',
- },
- {
- id: '5',
- briefingCode: 'SB-005',
- title: '마포 물류센터 증축',
- description: '마포구 상암동 물류센터 증축 현장설명회',
- partnerId: '2',
- partnerName: '삼성시공',
- briefingDate: '2025-05-15',
- briefingTime: '09:00',
- location: '상암 DMC 회의실',
- address: '서울특별시 마포구 상암산로 76',
- status: 'postponed',
- bidStatus: 'pending',
- bidDate: null,
- attendees: [],
- attendeeCount: 3,
- createdAt: '2025-01-05',
- updatedAt: '2025-01-05',
- createdBy: '이영희',
- },
-];
+ attendeeCount: apiData.attendee_count || 0,
+ attendanceStatus: (apiData.attendance_status as SiteBriefing['attendanceStatus']) || 'scheduled',
+ createdAt: apiData.created_at || '',
+ updatedAt: apiData.updated_at || '',
+ createdBy: apiData.created_by || '',
+ };
+}
+
+/**
+ * SiteBriefingFormData → API 요청 데이터 변환
+ */
+function transformFormDataToApi(data: SiteBriefingFormData): Record {
+ // attendeeItems 배열을 백엔드 형식으로 변환
+ // - id가 있으면 internal (직원), 없으면 external (외부인/직접입력)
+ const attendees = data.attendeeItems && data.attendeeItems.length > 0
+ ? data.attendeeItems.map(item => ({
+ ...item,
+ type: item.id ? 'internal' : 'external',
+ }))
+ : null;
+
+ return {
+ briefing_code: data.briefingCode,
+ title: data.projectName,
+ description: data.workReport,
+ partner_id: data.partnerId ? Number(data.partnerId) : null,
+ partner_name: data.partnerName,
+ briefing_date: data.briefingDate,
+ briefing_time: data.briefingTime,
+ briefing_type: data.briefingType,
+ location: data.location,
+ attendees: attendees, // 백엔드 필드명: attendees (복수형, array)
+ attendance_status: data.attendanceStatus,
+ project_name: data.projectName,
+ bid_date: data.bidDate,
+ site_count: data.siteCount,
+ construction_start_date: data.constructionStartDate,
+ construction_end_date: data.constructionEndDate,
+ vat_type: data.vatType,
+ work_report: data.workReport,
+ };
+}
// 현장설명회 목록 조회
export async function getSiteBriefingList(
filter?: SiteBriefingFilter
): Promise<{ success: boolean; data?: SiteBriefingListResponse; error?: string }> {
try {
- let filtered = [...mockSiteBriefings];
+ // API 쿼리 파라미터 구성 (모든 값을 문자열로 변환)
+ const params: Record = {
+ per_page: String(filter?.size ?? 20),
+ page: String(filter?.page ?? 1),
+ };
- // 검색 필터
if (filter?.search) {
- const search = filter.search.toLowerCase();
- filtered = filtered.filter(
- (b) =>
- b.title.toLowerCase().includes(search) ||
- b.briefingCode.toLowerCase().includes(search) ||
- b.partnerName.toLowerCase().includes(search)
- );
+ params.search = filter.search;
}
-
- // 상태 필터
if (filter?.status && filter.status !== 'all') {
- filtered = filtered.filter((b) => b.status === filter.status);
+ params.status = filter.status;
}
-
- // 입찰 상태 필터
if (filter?.bidStatus && filter.bidStatus !== 'all') {
- filtered = filtered.filter((b) => b.bidStatus === filter.bidStatus);
+ params.bid_status = filter.bidStatus;
}
-
- // 거래처 필터
if (filter?.partnerId) {
- filtered = filtered.filter((b) => b.partnerId === filter.partnerId);
+ params.partner_id = filter.partnerId;
}
-
- // 날짜 필터
if (filter?.startDate) {
- filtered = filtered.filter((b) => b.briefingDate >= filter.startDate!);
+ params.start_date = filter.startDate;
}
if (filter?.endDate) {
- filtered = filtered.filter((b) => b.briefingDate <= filter.endDate!);
+ params.end_date = filter.endDate;
}
-
- // 정렬
if (filter?.sortBy) {
- switch (filter.sortBy) {
- case 'latest':
- filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
- break;
- case 'oldest':
- filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
- break;
- case 'dateAsc':
- filtered.sort((a, b) => new Date(a.briefingDate).getTime() - new Date(b.briefingDate).getTime());
- break;
- case 'dateDesc':
- filtered.sort((a, b) => new Date(b.briefingDate).getTime() - new Date(a.briefingDate).getTime());
- break;
+ const sortMapping: Record = {
+ latest: { sort_by: 'created_at', sort_dir: 'desc' },
+ oldest: { sort_by: 'created_at', sort_dir: 'asc' },
+ dateAsc: { sort_by: 'briefing_date', sort_dir: 'asc' },
+ dateDesc: { sort_by: 'briefing_date', sort_dir: 'desc' },
+ };
+ const sort = sortMapping[filter.sortBy];
+ if (sort) {
+ params.sort_by = sort.sort_by;
+ params.sort_dir = sort.sort_dir;
}
}
- const page = filter?.page ?? 1;
- const size = filter?.size ?? 20;
- const start = (page - 1) * size;
- const paginatedItems = filtered.slice(start, start + size);
+ const response = await apiClient.get<{
+ success: boolean;
+ data: {
+ data: ApiSiteBriefing[];
+ current_page: number;
+ per_page: number;
+ total: number;
+ last_page: number;
+ };
+ }>('/site-briefings', { params });
+
+ const apiData = response.data;
+ const items = apiData.data.map(transformSiteBriefing);
return {
success: true,
data: {
- items: paginatedItems,
- total: filtered.length,
- page,
- size,
- totalPages: Math.ceil(filtered.length / size),
+ items,
+ total: apiData.total,
+ page: apiData.current_page,
+ size: apiData.per_page,
+ totalPages: apiData.last_page,
},
};
} catch (error) {
@@ -196,13 +197,12 @@ export async function getSiteBriefing(
id: string
): Promise<{ success: boolean; data?: SiteBriefing; error?: string }> {
try {
- const briefing = mockSiteBriefings.find((b) => b.id === id);
+ const response = await apiClient.get<{
+ success: boolean;
+ data: ApiSiteBriefing;
+ }>(`/site-briefings/${id}`);
- if (!briefing) {
- return { success: false, error: '현장설명회를 찾을 수 없습니다.' };
- }
-
- return { success: true, data: briefing };
+ return { success: true, data: transformSiteBriefing(response.data) };
} catch (error) {
console.error('getSiteBriefing error:', error);
return { success: false, error: '현장설명회 조회에 실패했습니다.' };
@@ -212,46 +212,86 @@ export async function getSiteBriefing(
// 현장설명회 통계 조회
export async function getSiteBriefingStats(): Promise<{ success: boolean; data?: SiteBriefingStats; error?: string }> {
try {
- const total = mockSiteBriefings.length;
- const scheduled = mockSiteBriefings.filter((b) => b.status === 'scheduled').length;
- const ongoing = mockSiteBriefings.filter((b) => b.status === 'ongoing').length;
- const completed = mockSiteBriefings.filter((b) => b.status === 'completed').length;
- const cancelled = mockSiteBriefings.filter((b) => b.status === 'cancelled' || b.status === 'postponed').length;
+ const response = await apiClient.get<{
+ success: boolean;
+ data: ApiSiteBriefingStats;
+ }>('/site-briefings/stats');
- return {
- success: true,
- data: {
- total,
- scheduled,
- ongoing,
- completed,
- cancelled,
- },
- };
+ return { success: true, data: response.data };
} catch (error) {
console.error('getSiteBriefingStats error:', error);
return { success: false, error: '통계 조회에 실패했습니다.' };
}
}
-// 현장설명회 삭제
+// ========================================
+// API 함수 (CRUD)
+// ========================================
+
+/**
+ * 현장설명회 등록
+ * POST /api/v1/site-briefings
+ */
+export async function createSiteBriefing(data: SiteBriefingFormData): Promise<{
+ success: boolean;
+ data?: SiteBriefing;
+ error?: string;
+}> {
+ try {
+ const apiData = transformFormDataToApi(data);
+ const response = await apiClient.post<{ success: boolean; data: ApiSiteBriefing }>('/site-briefings', apiData);
+ return { success: true, data: transformSiteBriefing(response.data) };
+ } catch (error) {
+ console.error('현장설명회 등록 오류:', error);
+ return { success: false, error: '현장설명회 등록에 실패했습니다.' };
+ }
+}
+
+/**
+ * 현장설명회 수정
+ * PUT /api/v1/site-briefings/{id}
+ */
+export async function updateSiteBriefing(id: string, data: SiteBriefingFormData): Promise<{
+ success: boolean;
+ data?: SiteBriefing;
+ error?: string;
+}> {
+ try {
+ const apiData = transformFormDataToApi(data);
+ const response = await apiClient.put<{ success: boolean; data: ApiSiteBriefing }>(`/site-briefings/${id}`, apiData);
+ return { success: true, data: transformSiteBriefing(response.data) };
+ } catch (error) {
+ console.error('현장설명회 수정 오류:', error);
+ return { success: false, error: '현장설명회 수정에 실패했습니다.' };
+ }
+}
+
+/**
+ * 현장설명회 삭제
+ * DELETE /api/v1/site-briefings/{id}
+ */
export async function deleteSiteBriefing(id: string): Promise<{ success: boolean; error?: string }> {
try {
- console.log('Delete site briefing:', id);
+ await apiClient.delete(`/site-briefings/${id}`);
return { success: true };
} catch (error) {
- console.error('deleteSiteBriefing error:', error);
+ console.error('현장설명회 삭제 오류:', error);
return { success: false, error: '현장설명회 삭제에 실패했습니다.' };
}
}
-// 현장설명회 일괄 삭제
+/**
+ * 현장설명회 일괄 삭제
+ * DELETE /api/v1/site-briefings/bulk
+ */
export async function deleteSiteBriefings(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
try {
- console.log('Delete site briefings:', ids);
+ await apiClient.delete('/site-briefings/bulk', {
+ data: { ids: ids.map((id) => Number(id)) },
+ });
return { success: true, deletedCount: ids.length };
} catch (error) {
- console.error('deleteSiteBriefings error:', error);
+ console.error('현장설명회 일괄 삭제 오류:', error);
return { success: false, error: '일괄 삭제에 실패했습니다.' };
}
}
diff --git a/src/components/business/construction/site-briefings/types.ts b/src/components/business/construction/site-briefings/types.ts
index af9beebd..ed7dbee3 100644
--- a/src/components/business/construction/site-briefings/types.ts
+++ b/src/components/business/construction/site-briefings/types.ts
@@ -8,7 +8,7 @@ export type SiteBriefingStatus = 'scheduled' | 'ongoing' | 'completed' | 'cancel
// 입찰 상태
export type BidStatus = 'pending' | 'bidding' | 'closed' | 'failed' | 'awarded';
-// 참석자 타입
+// 참석자 타입 (외부 참석자 - 상세 정보)
export interface Attendee {
id: string;
name: string;
@@ -18,6 +18,12 @@ export interface Attendee {
isAttended: boolean;
}
+// 참석자 항목 타입 (내부 직원 또는 직접 입력)
+export interface AttendeeItem {
+ id: string; // 직원 ID (직접 입력 시 빈 문자열)
+ name: string; // 이름
+}
+
// 현장설명회 타입
export interface SiteBriefing {
id: string;
@@ -41,8 +47,10 @@ export interface SiteBriefing {
bidDate: string | null; // 입찰 날짜
// 참석자 정보
+ attendee: string; // 참석자
attendees: Attendee[];
attendeeCount: number; // 참석자 수
+ attendanceStatus: AttendanceStatus; // 참석 상태
// 메타 정보
createdAt: string;
@@ -173,7 +181,7 @@ export interface SiteBriefingFormData {
briefingTime: string; // 현장설명회 시간
briefingType: BriefingType; // 구분 (온라인/오프라인)
location: string; // 현장설명회 장소
- attendee: string; // 참석자
+ attendeeItems: AttendeeItem[]; // 참석자 목록 (JSON으로 저장)
attendanceStatus: AttendanceStatus; // 상태
// 입찰 정보
@@ -219,7 +227,7 @@ export function getEmptySiteBriefingFormData(): SiteBriefingFormData {
briefingTime: '',
briefingType: 'offline',
location: '',
- attendee: '',
+ attendeeItems: [],
attendanceStatus: 'scheduled',
projectName: '',
bidDate: '',
@@ -233,6 +241,26 @@ export function getEmptySiteBriefingFormData(): SiteBriefingFormData {
};
}
+// attendee JSON 문자열을 AttendeeItem[]로 파싱
+export function parseAttendeeItems(attendeeJson: string | null): AttendeeItem[] {
+ if (!attendeeJson) return [];
+ try {
+ const parsed = JSON.parse(attendeeJson);
+ if (Array.isArray(parsed)) {
+ return parsed.filter((item): item is AttendeeItem =>
+ typeof item === 'object' && item !== null && typeof item.name === 'string'
+ );
+ }
+ return [];
+ } catch {
+ // JSON 파싱 실패 시 기존 단일 문자열을 AttendeeItem으로 변환
+ if (attendeeJson.trim()) {
+ return [{ id: '', name: attendeeJson.trim() }];
+ }
+ return [];
+ }
+}
+
// SiteBriefing을 FormData로 변환
export function siteBriefingToFormData(briefing: SiteBriefing): SiteBriefingFormData {
return {
@@ -243,8 +271,8 @@ export function siteBriefingToFormData(briefing: SiteBriefing): SiteBriefingForm
briefingTime: briefing.briefingTime,
briefingType: 'offline', // 기본값
location: briefing.location,
- attendee: '', // 기본값
- attendanceStatus: 'scheduled', // 기본값
+ attendeeItems: parseAttendeeItems(briefing.attendee),
+ attendanceStatus: briefing.attendanceStatus || 'scheduled',
projectName: briefing.title,
bidDate: briefing.bidDate || '',
siteCount: 0, // 기본값
diff --git a/src/components/business/construction/site-management/actions.ts b/src/components/business/construction/site-management/actions.ts
index edc3245b..2dab97ae 100644
--- a/src/components/business/construction/site-management/actions.ts
+++ b/src/components/business/construction/site-management/actions.ts
@@ -1,195 +1,249 @@
'use server';
-import type { Site, SiteStats } from './types';
+import type { Site, SiteStats, SiteStatus } from './types';
+import { apiClient } from '@/lib/api';
-// 목업 현장 데이터
-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
+ * 표준화된 apiClient 사용 버전
+ */
+
+// ========================================
+// API 응답 타입
+// ========================================
+
+interface ApiSite {
+ id: number;
+ site_code: string | null;
+ client_id: number | null;
+ name: string;
+ address: string | null;
+ status: SiteStatus;
+ created_at: string;
+ updated_at: string;
+ client?: {
+ id: number;
+ name: string;
+ } | null;
+}
+
+interface ApiSiteStats {
+ total: number;
+ construction: number;
+ unregistered: number;
+ suspended: number;
+ pending: number;
+}
+
+// ========================================
+// 타입 변환 함수
+// ========================================
+
+/**
+ * API 응답 → Site 타입 변환
+ */
+function transformSite(apiData: ApiSite): Site {
+ return {
+ id: String(apiData.id),
+ siteCode: apiData.site_code || '',
+ partnerId: apiData.client_id ? String(apiData.client_id) : '',
+ partnerName: apiData.client?.name || '',
+ siteName: apiData.name || '',
+ address: apiData.address || '',
+ status: apiData.status || 'unregistered',
+ createdAt: apiData.created_at || '',
+ updatedAt: apiData.updated_at || '',
+ };
+}
+
+// ========================================
+// API 함수
+// ========================================
interface GetSiteListParams {
size?: number;
+ page?: number;
startDate?: string;
endDate?: string;
+ search?: string;
+ status?: string;
+ clientId?: string;
+ sortBy?: string;
}
-interface GetSiteListResult {
+/**
+ * 현장 목록 조회
+ * GET /api/v1/sites
+ */
+export async function getSiteList(params: GetSiteListParams = {}): Promise<{
success: boolean;
data?: {
items: Site[];
- totalCount: number;
+ total: number;
+ page: number;
+ size: number;
+ totalPages: number;
};
error?: string;
-}
-
-// 현장 목록 조회
-export async function getSiteList(params: GetSiteListParams = {}): Promise {
+}> {
try {
- // TODO: API 연동 시 실제 API 호출로 변경
- await new Promise((resolve) => setTimeout(resolve, 500));
+ const queryParams: Record = {};
- let filteredSites = [...MOCK_SITES];
+ // 검색
+ if (params.search) queryParams.search = params.search;
- // 날짜 필터
- if (params.startDate) {
- filteredSites = filteredSites.filter(
- (site) => new Date(site.createdAt) >= new Date(params.startDate!)
- );
- }
- if (params.endDate) {
- filteredSites = filteredSites.filter(
- (site) => new Date(site.createdAt) <= new Date(params.endDate!)
- );
+ // 필터
+ if (params.status && params.status !== 'all') queryParams.status = params.status;
+ if (params.clientId && params.clientId !== 'all') queryParams.client_id = params.clientId;
+
+ // 날짜 범위
+ if (params.startDate) queryParams.start_date = params.startDate;
+ if (params.endDate) queryParams.end_date = params.endDate;
+
+ // 페이지네이션
+ if (params.page) queryParams.page = String(params.page);
+ if (params.size) queryParams.per_page = String(params.size);
+
+ // 정렬
+ if (params.sortBy) {
+ const sortMap: Record = {
+ 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.sort_by = sort.field;
+ queryParams.sort_dir = sort.dir;
+ }
}
+ const response = await apiClient.get<{
+ data: ApiSite[];
+ current_page: number;
+ per_page: number;
+ total: number;
+ last_page: number;
+ }>('/sites', { params: queryParams });
+
+ const items = (response.data || []).map(transformSite);
+
return {
success: true,
data: {
- items: filteredSites,
- totalCount: filteredSites.length,
+ items,
+ total: response.total || 0,
+ page: response.current_page || 1,
+ size: response.per_page || 20,
+ totalPages: response.last_page || 1,
},
};
} catch (error) {
- console.error('getSiteList error:', error);
- return {
- success: false,
- error: '현장 목록을 불러오는데 실패했습니다.',
- };
+ console.error('현장 목록 조회 오류:', error);
+ return { success: false, error: '현장 목록을 불러오는데 실패했습니다.' };
}
}
-// 현장 통계 조회
-export async function getSiteStats(): Promise<{ success: boolean; data?: SiteStats; error?: string }> {
+/**
+ * 현장 통계 조회
+ * GET /api/v1/sites/stats
+ */
+export async function getSiteStats(): Promise<{
+ success: boolean;
+ data?: SiteStats;
+ error?: string;
+}> {
try {
- // TODO: API 연동 시 실제 API 호출로 변경
- await new Promise((resolve) => setTimeout(resolve, 300));
-
- 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;
+ const response = await apiClient.get('/sites/stats');
return {
success: true,
data: {
- total,
- construction,
- unregistered,
+ total: response.total || 0,
+ construction: response.construction || 0,
+ unregistered: response.unregistered || 0,
+ suspended: response.suspended || 0,
+ pending: response.pending || 0,
},
};
} catch (error) {
- console.error('getSiteStats error:', error);
- return {
- success: false,
- error: '현장 통계를 불러오는데 실패했습니다.',
- };
+ console.error('현장 통계 조회 오류:', error);
+ return { success: false, error: '현장 통계를 불러오는데 실패했습니다.' };
}
}
-// 현장 삭제
-export async function deleteSite(id: string): Promise<{ success: boolean; error?: string }> {
+/**
+ * 현장 삭제
+ * DELETE /api/v1/sites/{id}
+ */
+export async function deleteSite(id: string): Promise<{
+ success: boolean;
+ error?: string;
+}> {
try {
- // TODO: API 연동 시 실제 API 호출로 변경
- await new Promise((resolve) => setTimeout(resolve, 500));
+ await apiClient.delete(`/sites/${id}`);
return { success: true };
} catch (error) {
- console.error('deleteSite error:', error);
- return {
- success: false,
- error: '현장 삭제에 실패했습니다.',
- };
+ console.error('현장 삭제 오류:', error);
+ return { success: false, error: '현장 삭제에 실패했습니다.' };
}
}
-// 현장 일괄 삭제
-export async function deleteSites(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
+/**
+ * 현장 일괄 삭제
+ * DELETE /api/v1/sites/bulk
+ */
+export async function deleteSites(ids: string[]): Promise<{
+ success: boolean;
+ deletedCount?: number;
+ error?: string;
+}> {
try {
- // TODO: API 연동 시 실제 API 호출로 변경
- await new Promise((resolve) => setTimeout(resolve, 500));
- return {
- success: true,
- deletedCount: ids.length,
- };
+ await apiClient.delete('/sites/bulk', {
+ data: { ids: ids.map((id) => Number(id)) },
+ });
+ return { success: true, deletedCount: ids.length };
} catch (error) {
- console.error('deleteSites error:', error);
- return {
- success: false,
- error: '현장 일괄 삭제에 실패했습니다.',
- };
+ console.error('현장 일괄 삭제 오류:', error);
+ return { success: false, error: '현장 일괄 삭제에 실패했습니다.' };
}
}
+
+// ========================================
+// 현장 생성/수정 타입
+// ========================================
+
+export interface CreateSiteData {
+ siteName: string;
+ partnerId: string;
+ address?: string;
+ status?: SiteStatus;
+}
+
+/**
+ * 현장 등록
+ * POST /api/v1/sites
+ */
+export async function createSite(data: CreateSiteData): Promise<{
+ success: boolean;
+ data?: Site;
+ error?: string;
+}> {
+ try {
+ const apiData = {
+ name: data.siteName,
+ client_id: data.partnerId ? Number(data.partnerId) : null,
+ address: data.address || null,
+ status: data.status || 'unregistered',
+ };
+
+ const response = await apiClient.post<{ success: boolean; data: ApiSite }>('/sites', apiData);
+ return { success: true, data: transformSite(response.data) };
+ } catch (error) {
+ console.error('현장 등록 오류:', error);
+ return { success: false, error: '현장 등록에 실패했습니다.' };
+ }
+}
\ No newline at end of file
diff --git a/src/components/business/construction/site-management/types.ts b/src/components/business/construction/site-management/types.ts
index 85eaabbc..5051d47c 100644
--- a/src/components/business/construction/site-management/types.ts
+++ b/src/components/business/construction/site-management/types.ts
@@ -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; // 보류 현장
}
// 상태 옵션
diff --git a/src/components/business/construction/structure-review/actions.ts b/src/components/business/construction/structure-review/actions.ts
index 5717198a..e5f5f952 100644
--- a/src/components/business/construction/structure-review/actions.ts
+++ b/src/components/business/construction/structure-review/actions.ts
@@ -1,184 +1,256 @@
'use server';
-import type { StructureReview, StructureReviewStats } from './types';
+import type { StructureReview, StructureReviewStats, StructureReviewStatus } from './types';
+import { apiClient } from '@/lib/api';
-// 목업 데이터
-const MOCK_STRUCTURE_REVIEWS: StructureReview[] = [
- {
- id: '1',
- reviewNumber: '123123',
- partnerId: '1',
- partnerName: '회사명',
- siteId: '1',
- siteName: '현장명',
- requestDate: '2025-12-12',
- reviewCompany: '회사명',
- reviewerName: '홍길동',
- reviewDate: '2025-12-15',
- completionDate: '2025-12-15',
- status: 'pending',
- createdAt: '2025-12-01T00:00:00Z',
- updatedAt: '2025-12-01T00:00:00Z',
- },
- {
- id: '2',
- reviewNumber: '123123',
- partnerId: '1',
- partnerName: '회사명',
- siteId: '2',
- siteName: '현장명',
- requestDate: '2025-12-12',
- reviewCompany: '회사명',
- reviewerName: '홍길동',
- reviewDate: '2025-12-15',
- completionDate: null,
- status: 'pending',
- createdAt: '2025-12-02T00:00:00Z',
- updatedAt: '2025-12-02T00:00:00Z',
- },
- {
- id: '3',
- reviewNumber: '123123',
- partnerId: '2',
- partnerName: '회사명',
- siteId: '3',
- siteName: '현장명',
- requestDate: '2025-12-12',
- reviewCompany: '회사명',
- reviewerName: '홍길동',
- reviewDate: null,
- completionDate: null,
- status: 'pending',
- createdAt: '2025-12-03T00:00:00Z',
- updatedAt: '2025-12-03T00:00:00Z',
- },
- {
- id: '4',
- reviewNumber: '123123',
- partnerId: '2',
- partnerName: '회사명',
- siteId: '4',
- siteName: '현장명',
- requestDate: '2025-12-12',
- reviewCompany: '회사명',
- reviewerName: '홍길동',
- reviewDate: '2025-12-15',
- completionDate: '2025-12-15',
- status: 'completed',
- createdAt: '2025-12-04T00:00:00Z',
- updatedAt: '2025-12-04T00:00:00Z',
- },
- {
- id: '5',
- reviewNumber: '123123',
- partnerId: '3',
- partnerName: '회사명',
- siteId: '5',
- siteName: '현장명',
- requestDate: '2025-12-12',
- reviewCompany: '회사명',
- reviewerName: '홍길동',
- reviewDate: '2025-12-15',
- completionDate: '2025-12-15',
- status: 'completed',
- createdAt: '2025-12-05T00:00:00Z',
- updatedAt: '2025-12-05T00:00:00Z',
- },
-];
+/**
+ * 구조검토관리 Server Actions
+ * 표준화된 apiClient 사용 버전
+ */
-// 구조검토 목록 조회
-export async function getStructureReviewList(params?: {
- size?: number;
- startDate?: string;
- endDate?: string;
-}): Promise<{
- success: boolean;
- data?: { items: StructureReview[]; total: number };
- error?: string;
-}> {
- // TODO: API 연동
- await new Promise((resolve) => setTimeout(resolve, 500));
+// ========================================
+// API 응답 타입
+// ========================================
+interface ApiStructureReview {
+ id: number;
+ review_number: string | null;
+ partner_id: number | null;
+ partner_name: string | null;
+ site_id: number | null;
+ site_name: string | null;
+ request_date: string | null;
+ review_company: string | null;
+ reviewer_name: string | null;
+ review_date: string | null;
+ completion_date: string | null;
+ status: StructureReviewStatus;
+ file_url: string | null;
+ created_at: string;
+ updated_at: string;
+}
+
+interface ApiStructureReviewStats {
+ total: number;
+ pending: number;
+ completed: number;
+}
+
+// ========================================
+// 타입 변환 함수
+// ========================================
+
+/**
+ * API 응답 → StructureReview 타입 변환
+ */
+function transformStructureReview(apiData: ApiStructureReview): StructureReview {
return {
- success: true,
- data: {
- items: MOCK_STRUCTURE_REVIEWS,
- total: MOCK_STRUCTURE_REVIEWS.length,
- },
+ id: String(apiData.id),
+ reviewNumber: apiData.review_number || '',
+ partnerId: apiData.partner_id ? String(apiData.partner_id) : '',
+ partnerName: apiData.partner_name || '',
+ siteId: apiData.site_id ? String(apiData.site_id) : '',
+ siteName: apiData.site_name || '',
+ requestDate: apiData.request_date || '',
+ reviewCompany: apiData.review_company || '',
+ reviewerName: apiData.reviewer_name || '',
+ reviewDate: apiData.review_date || null,
+ completionDate: apiData.completion_date || null,
+ status: apiData.status || 'pending',
+ fileUrl: apiData.file_url || undefined,
+ createdAt: apiData.created_at || '',
+ updatedAt: apiData.updated_at || '',
};
}
-// 구조검토 통계 조회
+/**
+ * StructureReview → API 요청 데이터 변환
+ */
+function transformToApiData(data: Partial): Record {
+ return {
+ review_number: data.reviewNumber || null,
+ partner_id: data.partnerId ? Number(data.partnerId) : null,
+ partner_name: data.partnerName || null,
+ site_id: data.siteId ? Number(data.siteId) : null,
+ site_name: data.siteName || null,
+ request_date: data.requestDate || null,
+ review_company: data.reviewCompany || null,
+ reviewer_name: data.reviewerName || null,
+ review_date: data.reviewDate || null,
+ completion_date: data.completionDate || null,
+ status: data.status || 'pending',
+ file_url: data.fileUrl || null,
+ };
+}
+
+// ========================================
+// API 함수
+// ========================================
+
+interface GetStructureReviewListParams {
+ size?: number;
+ page?: number;
+ startDate?: string;
+ endDate?: string;
+ search?: string;
+ status?: string;
+ partnerId?: string;
+ siteId?: string;
+ sortBy?: string;
+}
+
+/**
+ * 구조검토 목록 조회
+ * GET /api/v1/construction/structure-reviews
+ */
+export async function getStructureReviewList(params: GetStructureReviewListParams = {}): Promise<{
+ success: boolean;
+ data?: {
+ items: StructureReview[];
+ total: number;
+ page: number;
+ size: number;
+ totalPages: number;
+ };
+ error?: string;
+}> {
+ try {
+ const queryParams: Record = {};
+
+ // 검색
+ if (params.search) queryParams.search = params.search;
+
+ // 필터
+ if (params.status && params.status !== 'all') queryParams.status = params.status;
+ if (params.partnerId && params.partnerId !== 'all') queryParams.partner_id = params.partnerId;
+ if (params.siteId && params.siteId !== 'all') queryParams.site_id = params.siteId;
+
+ // 날짜 범위
+ if (params.startDate) queryParams.start_date = params.startDate;
+ if (params.endDate) queryParams.end_date = params.endDate;
+
+ // 페이지네이션
+ if (params.page) queryParams.page = String(params.page);
+ if (params.size) queryParams.per_page = String(params.size);
+
+ // 정렬
+ if (params.sortBy) {
+ const sortMap: Record = {
+ latest: { field: 'created_at', dir: 'desc' },
+ oldest: { field: 'created_at', dir: 'asc' },
+ partnerNameAsc: { field: 'partner_name', dir: 'asc' },
+ partnerNameDesc: { field: 'partner_name', dir: 'desc' },
+ siteNameAsc: { field: 'site_name', dir: 'asc' },
+ siteNameDesc: { field: 'site_name', dir: 'desc' },
+ };
+ const sort = sortMap[params.sortBy];
+ if (sort) {
+ queryParams.sort_by = sort.field;
+ queryParams.sort_dir = sort.dir;
+ }
+ }
+
+ const response = await apiClient.get<{
+ data: ApiStructureReview[];
+ current_page: number;
+ per_page: number;
+ total: number;
+ last_page: number;
+ }>('/construction/structure-reviews', { params: queryParams });
+
+ const items = (response.data || []).map(transformStructureReview);
+
+ return {
+ success: true,
+ data: {
+ items,
+ total: response.total || 0,
+ page: response.current_page || 1,
+ size: response.per_page || 20,
+ totalPages: response.last_page || 1,
+ },
+ };
+ } catch (error) {
+ console.error('구조검토 목록 조회 오류:', error);
+ return { success: false, error: '구조검토 목록을 불러오는데 실패했습니다.' };
+ }
+}
+
+/**
+ * 구조검토 통계 조회
+ * GET /api/v1/construction/structure-reviews/stats
+ */
export async function getStructureReviewStats(): Promise<{
success: boolean;
data?: StructureReviewStats;
error?: string;
}> {
- // TODO: API 연동
- await new Promise((resolve) => setTimeout(resolve, 300));
+ try {
+ const response = await apiClient.get('/construction/structure-reviews/stats');
- const pending = MOCK_STRUCTURE_REVIEWS.filter((r) => r.status === 'pending').length;
- const completed = MOCK_STRUCTURE_REVIEWS.filter((r) => r.status === 'completed').length;
-
- return {
- success: true,
- data: {
- total: MOCK_STRUCTURE_REVIEWS.length,
- pending,
- completed,
- },
- };
+ return {
+ success: true,
+ data: {
+ total: response.total || 0,
+ pending: response.pending || 0,
+ completed: response.completed || 0,
+ },
+ };
+ } catch (error) {
+ console.error('구조검토 통계 조회 오류:', error);
+ return { success: false, error: '구조검토 통계를 불러오는데 실패했습니다.' };
+ }
}
-// 구조검토 상세 조회
+/**
+ * 구조검토 상세 조회
+ * GET /api/v1/construction/structure-reviews/{id}
+ */
export async function getStructureReview(id: string): Promise<{
success: boolean;
data?: StructureReview;
error?: string;
}> {
- // TODO: API 연동
- await new Promise((resolve) => setTimeout(resolve, 300));
+ try {
+ const response = await apiClient.get(`/construction/structure-reviews/${id}`);
- const review = MOCK_STRUCTURE_REVIEWS.find((r) => r.id === id);
-
- if (!review) {
+ return {
+ success: true,
+ data: transformStructureReview(response),
+ };
+ } catch (error) {
+ console.error('구조검토 상세 조회 오류:', error);
return { success: false, error: '구조검토 정보를 찾을 수 없습니다.' };
}
-
- return { success: true, data: review };
}
-// 구조검토 생성
+/**
+ * 구조검토 생성
+ * POST /api/v1/construction/structure-reviews
+ */
export async function createStructureReview(data: Partial): Promise<{
success: boolean;
data?: StructureReview;
error?: string;
}> {
- // TODO: API 연동
- await new Promise((resolve) => setTimeout(resolve, 500));
+ try {
+ const apiData = transformToApiData(data);
+ const response = await apiClient.post('/construction/structure-reviews', apiData);
- return {
- success: true,
- data: {
- id: String(Date.now()),
- reviewNumber: data.reviewNumber || '',
- partnerId: data.partnerId || '',
- partnerName: data.partnerName || '',
- siteId: data.siteId || '',
- siteName: data.siteName || '',
- requestDate: data.requestDate || '',
- reviewCompany: data.reviewCompany || '',
- reviewerName: data.reviewerName || '',
- reviewDate: data.reviewDate || null,
- completionDate: data.completionDate || null,
- status: data.status || 'pending',
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- },
- };
+ return {
+ success: true,
+ data: transformStructureReview(response),
+ };
+ } catch (error) {
+ console.error('구조검토 생성 오류:', error);
+ return { success: false, error: '구조검토 등록에 실패했습니다.' };
+ }
}
-// 구조검토 수정
+/**
+ * 구조검토 수정
+ * PUT /api/v1/construction/structure-reviews/{id}
+ */
export async function updateStructureReview(
id: string,
data: Partial
@@ -187,43 +259,53 @@ export async function updateStructureReview(
data?: StructureReview;
error?: string;
}> {
- // TODO: API 연동
- await new Promise((resolve) => setTimeout(resolve, 500));
+ try {
+ const apiData = transformToApiData(data);
+ const response = await apiClient.put(`/construction/structure-reviews/${id}`, apiData);
- const existing = MOCK_STRUCTURE_REVIEWS.find((r) => r.id === id);
- if (!existing) {
- return { success: false, error: '구조검토 정보를 찾을 수 없습니다.' };
+ return {
+ success: true,
+ data: transformStructureReview(response),
+ };
+ } catch (error) {
+ console.error('구조검토 수정 오류:', error);
+ return { success: false, error: '구조검토 수정에 실패했습니다.' };
}
-
- return {
- success: true,
- data: {
- ...existing,
- ...data,
- updatedAt: new Date().toISOString(),
- },
- };
}
-// 구조검토 삭제
+/**
+ * 구조검토 삭제
+ * DELETE /api/v1/construction/structure-reviews/{id}
+ */
export async function deleteStructureReview(id: string): Promise<{
success: boolean;
error?: string;
}> {
- // TODO: API 연동
- await new Promise((resolve) => setTimeout(resolve, 500));
-
- return { success: true };
+ try {
+ await apiClient.delete(`/construction/structure-reviews/${id}`);
+ return { success: true };
+ } catch (error) {
+ console.error('구조검토 삭제 오류:', error);
+ return { success: false, error: '구조검토 삭제에 실패했습니다.' };
+ }
}
-// 구조검토 일괄 삭제
+/**
+ * 구조검토 일괄 삭제
+ * DELETE /api/v1/construction/structure-reviews/bulk
+ */
export async function deleteStructureReviews(ids: string[]): Promise<{
success: boolean;
deletedCount?: number;
error?: string;
}> {
- // TODO: API 연동
- await new Promise((resolve) => setTimeout(resolve, 500));
-
- return { success: true, deletedCount: ids.length };
+ try {
+ await apiClient.delete('/construction/structure-reviews/bulk', {
+ data: { ids: ids.map((id) => Number(id)) },
+ });
+ return { success: true, deletedCount: ids.length };
+ } catch (error) {
+ console.error('구조검토 일괄 삭제 오류:', error);
+ return { success: false, error: '구조검토 일괄 삭제에 실패했습니다.' };
+ }
}
\ No newline at end of file
diff --git a/src/components/hr/EmployeeManagement/EmployeeForm.tsx b/src/components/hr/EmployeeManagement/EmployeeForm.tsx
index 3d3cc533..116f2dac 100644
--- a/src/components/hr/EmployeeManagement/EmployeeForm.tsx
+++ b/src/components/hr/EmployeeManagement/EmployeeForm.tsx
@@ -311,6 +311,32 @@ export function EmployeeForm({
}));
};
+ // 부서 선택 변경 (id와 name 모두 업데이트)
+ const handleDepartmentSelect = (dpId: string, departmentId: string) => {
+ const dept = departments.find(d => String(d.id) === departmentId);
+ if (dept) {
+ setFormData(prev => ({
+ ...prev,
+ departmentPositions: prev.departmentPositions.map(dp =>
+ dp.id === dpId ? { ...dp, departmentId: String(dept.id), departmentName: dept.name } : dp
+ ),
+ }));
+ }
+ };
+
+ // 직책 선택 변경 (id와 name 모두 업데이트)
+ const handlePositionSelect = (dpId: string, positionId: string) => {
+ const position = titles.find(t => String(t.id) === positionId);
+ if (position) {
+ setFormData(prev => ({
+ ...prev,
+ departmentPositions: prev.departmentPositions.map(dp =>
+ dp.id === dpId ? { ...dp, positionId: String(position.id), positionName: position.name } : dp
+ ),
+ }));
+ }
+ };
+
// 저장
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -624,13 +650,20 @@ export function EmployeeForm({
{fieldSettings.showRank && (
- handleChange('rank', e.target.value)}
- placeholder="직급 입력"
+ onValueChange={(value) => handleChange('rank', value)}
disabled={isViewMode}
- />
+ >
+
+
+
+
+ {ranks.map((rank) => (
+ {rank.name}
+ ))}
+
+
)}
@@ -681,20 +714,38 @@ export function EmployeeForm({
{formData.departmentPositions.map((dp) => (
- handleDepartmentPositionChange(dp.id, 'departmentName', e.target.value)}
- placeholder="부서명"
- className="flex-1"
+
+
{!isViewMode && (
@@ -448,7 +452,7 @@ export function OrderRegistration({
@@ -855,7 +857,7 @@ export function OrderRegistration({
총금액:
- {formatAmount(form.totalAmount)}원
+ {formatAmount(form.totalAmount)}
diff --git a/src/components/orders/QuotationSelectDialog.tsx b/src/components/orders/QuotationSelectDialog.tsx
index e930155e..952e8c53 100644
--- a/src/components/orders/QuotationSelectDialog.tsx
+++ b/src/components/orders/QuotationSelectDialog.tsx
@@ -4,9 +4,10 @@
* 견적 선택 팝업
*
* 확정된 견적 목록에서 수주 전환할 견적을 선택하는 다이얼로그
+ * API 연동: getQuotesForSelect (FINALIZED 상태 견적만 조회)
*/
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
import {
Dialog,
DialogContent,
@@ -15,37 +16,10 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
-import { Search, FileText, Check } from "lucide-react";
+import { Search, FileText, Check, Loader2 } from "lucide-react";
import { formatAmount } from "@/utils/formatAmount";
import { cn } from "@/lib/utils";
-
-// 견적 타입
-export interface QuotationForSelect {
- id: string;
- quoteNumber: string; // KD-PR-XXXXXX-XX
- grade: string; // A(우량), B(관리), C(주의)
- client: string; // 발주처
- siteName: string; // 현장명
- amount: number; // 총 금액
- itemCount: number; // 품목 수
- registrationDate: string; // 견적일
- manager?: string; // 담당자
- contact?: string; // 연락처
- items?: QuotationItem[]; // 품목 내역
-}
-
-export interface QuotationItem {
- id: string;
- itemCode: string;
- itemName: string;
- type: string; // 종
- symbol: string; // 부호
- spec: string; // 규격
- quantity: number;
- unit: string;
- unitPrice: number;
- amount: number;
-}
+import { getQuotesForSelect, type QuotationForSelect } from "./actions";
interface QuotationSelectDialogProps {
open: boolean;
@@ -54,81 +28,6 @@ interface QuotationSelectDialogProps {
selectedId?: string;
}
-// 샘플 견적 데이터 (실제 구현에서는 API 연동)
-const SAMPLE_QUOTATIONS: QuotationForSelect[] = [
- {
- id: "QT-001",
- quoteNumber: "KD-PR-251210-01",
- grade: "A",
- client: "태영건설(주)",
- siteName: "데시앙 동탄 파크뷰",
- amount: 38800000,
- itemCount: 5,
- registrationDate: "2024-12-10",
- manager: "김철수",
- contact: "010-1234-5678",
- items: [
- { id: "1", itemCode: "PRD-001", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS1", spec: "7260×2600", quantity: 2, unit: "EA", unitPrice: 8000000, amount: 16000000 },
- { id: "2", itemCode: "PRD-002", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS2", spec: "5000×2400", quantity: 3, unit: "EA", unitPrice: 7600000, amount: 22800000 },
- ],
- },
- {
- id: "QT-002",
- quoteNumber: "KD-PR-251211-02",
- grade: "A",
- client: "현대건설(주)",
- siteName: "힐스테이트 판교역",
- amount: 52500000,
- itemCount: 8,
- registrationDate: "2024-12-11",
- manager: "이영희",
- contact: "010-2345-6789",
- items: [
- { id: "1", itemCode: "PRD-003", itemName: "국민방화스크린세터", type: "B2", symbol: "FSS1", spec: "6000×3000", quantity: 4, unit: "EA", unitPrice: 9500000, amount: 38000000 },
- { id: "2", itemCode: "PRD-004", itemName: "국민방화스크린세터", type: "B1", symbol: "FSS2", spec: "4500×2500", quantity: 2, unit: "EA", unitPrice: 7250000, amount: 14500000 },
- ],
- },
- {
- id: "QT-003",
- quoteNumber: "KD-PR-251208-03",
- grade: "B",
- client: "GS건설(주)",
- siteName: "자이 강남센터",
- amount: 45000000,
- itemCount: 6,
- registrationDate: "2024-12-08",
- manager: "박민수",
- contact: "010-3456-7890",
- items: [],
- },
- {
- id: "QT-004",
- quoteNumber: "KD-PR-251205-04",
- grade: "B",
- client: "대우건설(주)",
- siteName: "푸르지오 송도",
- amount: 28900000,
- itemCount: 4,
- registrationDate: "2024-12-05",
- manager: "최지원",
- contact: "010-4567-8901",
- items: [],
- },
- {
- id: "QT-005",
- quoteNumber: "KD-PR-251201-05",
- grade: "A",
- client: "포스코건설",
- siteName: "더샵 분당센트럴",
- amount: 62000000,
- itemCount: 10,
- registrationDate: "2024-12-01",
- manager: "정수민",
- contact: "010-5678-9012",
- items: [],
- },
-];
-
// 등급 배지 컴포넌트
function GradeBadge({ grade }: { grade: string }) {
const config: Record