From 9d30555265cd85550053455551b4c741aa13d03f Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 9 Jan 2026 16:08:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EC=8B=9C=EA=B3=B5=EC=82=AC):=201.2=20?= =?UTF-8?q?=EC=9D=B8=EC=88=98=EC=9D=B8=EA=B3=84=EB=B3=B4=EA=B3=A0=EC=84=9C?= =?UTF-8?q?=20-=20Frontend=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mock 데이터 제거, 실제 API 연동으로 변환 - apiRequest 헬퍼 함수 구현 (쿠키 기반 인증) - 7개 API 함수 구현: list, stats, detail, create, update, delete, bulk-delete - snake_case → camelCase 타입 변환 함수 추가 --- .../construction/handover-report/actions.ts | 681 ++++++++++-------- 1 file changed, 390 insertions(+), 291 deletions(-) diff --git a/src/components/business/construction/handover-report/actions.ts b/src/components/business/construction/handover-report/actions.ts index 21208cf7..938f1ac0 100644 --- a/src/components/business/construction/handover-report/actions.ts +++ b/src/components/business/construction/handover-report/actions.ts @@ -1,128 +1,243 @@ 'use server'; -import type { HandoverReport, HandoverReportStats, HandoverReportDetail, HandoverReportFormData } from './types'; +import { cookies } from 'next/headers'; +import type { + HandoverReport, + HandoverReportDetail, + HandoverReportStats, + HandoverReportFormData, + ConstructionManager, + ContractItem, + ExternalEquipmentCost, +} from './types'; -// 목업 데이터 -const MOCK_REPORTS: HandoverReport[] = [ - { - id: '1', - reportNumber: '123123', - partnerName: '통신공사', - siteName: '서울역사 통신공사', - contractManagerName: '홍길동', - constructionPMName: '김PM', - totalSites: 21, - contractAmount: 105800000, - contractStartDate: '2025-12-12', - contractEndDate: '2026-12-12', - status: 'pending', - contractId: '1', - createdAt: '2025-01-01', - updatedAt: '2025-01-01', - }, - { - id: '2', - reportNumber: '123124', - partnerName: '야사건설', - siteName: '부산항 건설현장', - contractManagerName: '김철수', - constructionPMName: '이PM', - totalSites: 15, - contractAmount: 10500000, - contractStartDate: '2025-11-01', - contractEndDate: '2026-11-01', - status: 'completed', - contractId: '2', - createdAt: '2025-01-02', - updatedAt: '2025-01-02', - }, - { - id: '3', - reportNumber: '123125', - partnerName: '여의건설', - siteName: '인천공항 확장공사', - contractManagerName: '이영희', - constructionPMName: '박PM', - totalSites: 30, - contractAmount: 10000000, - contractStartDate: '2025-10-15', - contractEndDate: '2026-10-15', - status: 'pending', - contractId: '3', - createdAt: '2025-01-03', - updatedAt: '2025-01-03', - }, - { - id: '4', - reportNumber: '123126', - partnerName: '통신공사', - siteName: '대전역 리모델링', - contractManagerName: '홍길동', - constructionPMName: '김PM', - totalSites: 18, - contractAmount: 10000000, - contractStartDate: '2025-09-20', - contractEndDate: '2026-03-20', - status: 'completed', - contractId: '4', - createdAt: '2025-01-04', - updatedAt: '2025-01-04', - }, - { - id: '5', - reportNumber: '123127', - partnerName: '야사건설', - siteName: '광주 신축현장', - contractManagerName: '김철수', - constructionPMName: '이PM', - totalSites: 17, - contractAmount: 10500000, - contractStartDate: '2025-08-01', - contractEndDate: '2026-08-01', - status: 'pending', - contractId: '5', - createdAt: '2025-01-05', - updatedAt: '2025-01-05', - }, - { - id: '6', - reportNumber: '123128', - partnerName: '여의건설', - siteName: '세종시 행정타운', - contractManagerName: '이영희', - constructionPMName: '박PM', - totalSites: 25, - contractAmount: 100000000, - contractStartDate: '2025-07-15', - contractEndDate: '2026-07-15', - status: 'completed', - contractId: '6', - createdAt: '2025-01-06', - updatedAt: '2025-01-06', - }, - { - id: '7', - reportNumber: '123129', - partnerName: '통신공사', - siteName: '제주 관광단지', - contractManagerName: '홍길동', - constructionPMName: null, - totalSites: 12, - contractAmount: 105800000, - contractStartDate: '2025-06-01', - contractEndDate: '2026-06-01', - status: 'pending', - contractId: '7', - createdAt: '2025-01-07', - updatedAt: '2025-01-07', - }, -]; +/** + * 주일 기업 - 인수인계보고서관리 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( + 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 = { + '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('🔵 [HandoverReport API]', options.method || 'GET', url); + + const response = await fetch(url, { + ...options, + headers: { + ...headers, + ...options.headers, + }, + }); + + const result = await response.json(); + console.log('🔵 [HandoverReport 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 transformHandoverReport(apiData: Record): HandoverReport { + return { + id: String(apiData.id), + reportNumber: String(apiData.report_number || ''), + partnerName: String(apiData.partner_name || ''), + siteName: String(apiData.site_name || ''), + contractManagerName: String(apiData.contract_manager_name || ''), + constructionPMName: apiData.construction_pm_name ? String(apiData.construction_pm_name) : null, + totalSites: Number(apiData.total_sites || 0), + contractAmount: Number(apiData.contract_amount || 0), + contractStartDate: apiData.contract_start_date ? String(apiData.contract_start_date) : null, + contractEndDate: apiData.contract_end_date ? String(apiData.contract_end_date) : null, + status: (apiData.status as 'pending' | 'completed') || 'pending', + contractId: String(apiData.contract_id || ''), + createdAt: String(apiData.created_at || ''), + updatedAt: String(apiData.updated_at || ''), + }; +} + +/** + * API 응답 → 프론트엔드 타입 변환 (상세용) + */ +function transformHandoverReportDetail(apiData: Record): HandoverReportDetail { + // 공사담당자 목록 변환 + const managersData = apiData.managers as Record[] | undefined; + const constructionManagers: ConstructionManager[] = (managersData || []).map((m) => ({ + id: String(m.id || ''), + name: String(m.name || ''), + nonPerformanceReason: String(m.non_performance_reason || ''), + signature: m.signature ? String(m.signature) : null, + })); + + // 계약 ITEM 목록 변환 + const itemsData = apiData.items as Record[] | undefined; + const contractItems: ContractItem[] = (itemsData || []).map((item) => ({ + id: String(item.id || ''), + no: Number(item.item_no || item.no || 0), + name: String(item.name || ''), + product: String(item.product || ''), + quantity: Number(item.quantity || 0), + remark: String(item.remark || ''), + })); + + // 장비 외 실행금액 변환 + const externalCostData = apiData.external_equipment_cost as Record | undefined; + const externalEquipmentCost: ExternalEquipmentCost = externalCostData + ? { + shippingCost: Number(externalCostData.shipping_cost || externalCostData.shippingCost || 0), + highAltitudeWork: Number(externalCostData.high_altitude_work || externalCostData.highAltitudeWork || 0), + publicExpense: Number(externalCostData.public_expense || externalCostData.publicExpense || 0), + } + : { + shippingCost: 0, + highAltitudeWork: 0, + publicExpense: 0, + }; + + return { + id: String(apiData.id), + reportNumber: String(apiData.report_number || ''), + partnerName: String(apiData.partner_name || ''), + siteName: String(apiData.site_name || ''), + contractManagerName: String(apiData.contract_manager_name || ''), + constructionPMName: apiData.construction_pm_name ? String(apiData.construction_pm_name) : null, + constructionPMId: apiData.construction_pm_id ? String(apiData.construction_pm_id) : null, + totalSites: Number(apiData.total_sites || 0), + contractAmount: Number(apiData.contract_amount || 0), + contractDate: apiData.contract_date ? String(apiData.contract_date) : null, + contractStartDate: apiData.contract_start_date ? String(apiData.contract_start_date) : null, + contractEndDate: apiData.contract_end_date ? String(apiData.contract_end_date) : null, + completionDate: apiData.completion_date ? String(apiData.completion_date) : null, + status: (apiData.status as 'pending' | 'completed') || 'pending', + contractId: String(apiData.contract_id || ''), + createdAt: String(apiData.created_at || ''), + updatedAt: String(apiData.updated_at || ''), + constructionManagers, + contractItems, + hasSecondaryPiping: Boolean(apiData.has_secondary_piping), + secondaryPipingAmount: Number(apiData.secondary_piping_amount || 0), + secondaryPipingNote: String(apiData.secondary_piping_note || ''), + hasCoating: Boolean(apiData.has_coating), + coatingAmount: Number(apiData.coating_amount || 0), + coatingNote: String(apiData.coating_note || ''), + externalEquipmentCost, + specialNotes: String(apiData.special_notes || ''), + }; +} + +/** + * 프론트엔드 → API 요청 타입 변환 + */ +function transformToApiRequest(data: Partial): Record { + const apiData: Record = {}; + + if (data.reportNumber !== undefined) apiData.report_number = data.reportNumber; + if (data.partnerName !== undefined) apiData.partner_name = data.partnerName || null; + if (data.siteName !== undefined) apiData.site_name = data.siteName; + if (data.contractManagerName !== undefined) apiData.contract_manager_name = data.contractManagerName || null; + if (data.contractDate !== undefined) apiData.contract_date = data.contractDate || null; + if (data.totalSites !== undefined) apiData.total_sites = data.totalSites; + if (data.contractStartDate !== undefined) apiData.contract_start_date = data.contractStartDate || null; + if (data.contractEndDate !== undefined) apiData.contract_end_date = data.contractEndDate || null; + if (data.contractAmount !== undefined) apiData.contract_amount = data.contractAmount; + if (data.constructionPMId !== undefined) apiData.construction_pm_id = data.constructionPMId || null; + if (data.constructionPMName !== undefined) apiData.construction_pm_name = data.constructionPMName || null; + if (data.status !== undefined) apiData.status = data.status; + if (data.hasSecondaryPiping !== undefined) apiData.has_secondary_piping = data.hasSecondaryPiping; + if (data.secondaryPipingNote !== undefined) apiData.secondary_piping_note = data.secondaryPipingNote || null; + if (data.hasCoating !== undefined) apiData.has_coating = data.hasCoating; + if (data.coatingNote !== undefined) apiData.coating_note = data.coatingNote || null; + if (data.specialNotes !== undefined) apiData.special_notes = data.specialNotes || null; + + // 장비 외 실행금액 변환 + if (data.externalEquipmentCost !== undefined) { + apiData.external_equipment_cost = { + shipping_cost: data.externalEquipmentCost.shippingCost, + high_altitude_work: data.externalEquipmentCost.highAltitudeWork, + public_expense: data.externalEquipmentCost.publicExpense, + }; + } + + // 공사담당자 변환 + if (data.constructionManagers !== undefined) { + apiData.managers = data.constructionManagers.map((m) => ({ + name: m.name, + non_performance_reason: m.nonPerformanceReason || null, + signature: m.signature || null, + })); + } + + // 계약 ITEM 변환 + if (data.contractItems !== undefined) { + apiData.items = data.contractItems.map((item, index) => ({ + item_no: item.no || index + 1, + name: item.name, + product: item.product || null, + quantity: item.quantity, + remark: item.remark || null, + })); + } + + return apiData; +} + +// ============================================================ +// API 연동 함수 +// ============================================================ interface GetHandoverReportListParams { size?: number; page?: number; startDate?: string; endDate?: string; + search?: string; + status?: string; + partnerId?: string; + contractManagerId?: string; + constructionPMId?: string; + sortBy?: string; } interface GetHandoverReportListResult { @@ -132,33 +247,78 @@ interface GetHandoverReportListResult { total: number; page: number; size: number; + totalPages: number; }; error?: string; } +/** + * 인수인계보고서 목록 조회 + */ export async function getHandoverReportList( params: GetHandoverReportListParams = {} ): Promise { try { - // 실제 API 호출 시 여기에 구현 - // const response = await fetch(`/api/v1/handover-reports?...`); + const queryParams = new URLSearchParams(); + + if (params.search) queryParams.append('search', params.search); + if (params.status && params.status !== 'all') queryParams.append('status', params.status); + if (params.partnerId && params.partnerId !== 'all') queryParams.append('partner_id', params.partnerId); + if (params.contractManagerId && params.contractManagerId !== 'all') queryParams.append('contract_manager_id', params.contractManagerId); + if (params.constructionPMId && params.constructionPMId !== 'all') queryParams.append('construction_pm_id', params.constructionPMId); + 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.sortBy) { + const sortMap: Record = { + contractDateDesc: { field: 'contract_start_date', dir: 'desc' }, + contractDateAsc: { field: 'contract_start_date', 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.append('sort_by', sort.field); + queryParams.append('sort_dir', sort.dir); + } + } + + const queryString = queryParams.toString(); + const endpoint = `/construction/handover-reports${queryString ? `?${queryString}` : ''}`; + + const result = await apiRequest<{ + data: Record[]; + 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(transformHandoverReport); - // 목업 데이터 반환 return { success: true, data: { - items: MOCK_REPORTS, - total: MOCK_REPORTS.length, - page: params.page || 1, - size: params.size || 20, + items, + total: apiData.total || 0, + page: apiData.current_page || 1, + size: apiData.per_page || 20, + totalPages: apiData.last_page || 1, }, }; } catch (error) { - console.error('Failed to fetch handover report list:', error); - return { - success: false, - error: '인수인계보고서 목록을 불러오는데 실패했습니다.', - }; + console.error('getHandoverReportList error:', error); + return { success: false, error: '인수인계보고서 목록을 불러오는데 실패했습니다.' }; } } @@ -168,28 +328,34 @@ interface GetHandoverReportStatsResult { error?: string; } +/** + * 인수인계보고서 통계 조회 + */ export async function getHandoverReportStats(): Promise { try { - // 실제 API 호출 시 여기에 구현 + const result = await apiRequest<{ + total_count: number; + pending_count: number; + completed_count: number; + total_amount?: number; + total_sites?: number; + }>('/construction/handover-reports/stats'); - // 목업 통계 반환 - const pending = MOCK_REPORTS.filter(r => r.status === 'pending').length; - const completed = MOCK_REPORTS.filter(r => r.status === 'completed').length; + if (!result.success || !result.data) { + return { success: false, error: result.error || '통계를 불러오는데 실패했습니다.' }; + } return { success: true, data: { - total: MOCK_REPORTS.length, - pending, - completed, + total: result.data.total_count || 0, + pending: result.data.pending_count || 0, + completed: result.data.completed_count || 0, }, }; } catch (error) { - console.error('Failed to fetch handover report stats:', error); - return { - success: false, - error: '통계를 불러오는데 실패했습니다.', - }; + console.error('getHandoverReportStats error:', error); + return { success: false, error: '통계를 불러오는데 실패했습니다.' }; } } @@ -198,20 +364,23 @@ interface DeleteHandoverReportResult { error?: string; } +/** + * 인수인계보고서 삭제 + */ export async function deleteHandoverReport(id: string): Promise { try { - // 실제 API 호출 시 여기에 구현 - console.log('Deleting handover report:', id); + const result = await apiRequest(`/construction/handover-reports/${id}`, { + method: 'DELETE', + }); - return { - success: true, - }; + if (!result.success) { + return { success: false, error: result.error || '삭제에 실패했습니다.' }; + } + + return { success: true }; } catch (error) { - console.error('Failed to delete handover report:', error); - return { - success: false, - error: '삭제에 실패했습니다.', - }; + console.error('deleteHandoverReport error:', error); + return { success: false, error: '삭제에 실패했습니다.' }; } } @@ -221,180 +390,110 @@ interface DeleteHandoverReportsResult { error?: string; } +/** + * 인수인계보고서 일괄 삭제 + */ export async function deleteHandoverReports(ids: string[]): Promise { try { - // 실제 API 호출 시 여기에 구현 - console.log('Deleting handover reports:', ids); + const result = await apiRequest('/construction/handover-reports/bulk', { + method: 'DELETE', + body: JSON.stringify({ ids: ids.map((id) => Number(id)) }), + }); - return { - success: true, - deletedCount: ids.length, - }; + if (!result.success) { + return { success: false, error: result.error || '일괄 삭제에 실패했습니다.' }; + } + + return { success: true, deletedCount: ids.length }; } catch (error) { - console.error('Failed to delete handover reports:', error); - return { - success: false, - error: '일괄 삭제에 실패했습니다.', - }; + console.error('deleteHandoverReports error:', error); + return { success: false, error: '일괄 삭제에 실패했습니다.' }; } } -// 목업 상세 데이터 -const MOCK_REPORT_DETAILS: Record = { - '1': { - id: '1', - reportNumber: '123123', - partnerName: '통신공사', - siteName: '서울역사 통신공사', - contractManagerName: '홍길동', - constructionPMName: '김PM', - constructionPMId: 'pm1', - totalSites: 21, - contractAmount: 105800000, - contractDate: '2025-12-12', - contractStartDate: '2026-01-01', - contractEndDate: '2026-12-10', - status: 'pending', - contractId: '1', - createdAt: '2025-01-01', - updatedAt: '2025-01-01', - completionDate: '2026-05-01', - constructionManagers: [ - { id: 'mgr1', name: '홍길동', isNonPerformanceUsed: false }, - { id: 'mgr2', name: '김철수', isNonPerformanceUsed: true }, - ], - contractItems: [ - { id: 'item1', no: 1, name: '접지방화서터', product: '제품', quantity: 1000, remark: '품질인증적용' }, - { id: 'item2', no: 2, name: '스크린방화서터', product: '제품', quantity: 111, remark: '품질인증적용' }, - ], - hasSecondaryPiping: true, - secondaryPipingAmount: 1200000, - hasCoating: true, - coatingAmount: 500000, - externalEquipmentCost: { - shippingCost: 1500000, - highAltitudeWork: 800000, - publicExpense: 10000000, - }, - specialNotes: '특이사항 내용이 여기에 표시됩니다.', - }, - '2': { - id: '2', - reportNumber: '123124', - partnerName: '야사건설', - siteName: '부산항 건설현장', - contractManagerName: '김철수', - constructionPMName: '이PM', - constructionPMId: 'pm2', - totalSites: 15, - contractAmount: 10500000, - contractDate: '2025-11-01', - contractStartDate: '2025-11-01', - contractEndDate: '2026-11-01', - status: 'completed', - contractId: '2', - createdAt: '2025-01-02', - updatedAt: '2025-01-02', - completionDate: '2026-04-01', - constructionManagers: [ - { id: 'mgr3', name: '이영희', isNonPerformanceUsed: false }, - ], - contractItems: [ - { id: 'item3', no: 1, name: '방화문', product: '제품A', quantity: 500, remark: '' }, - ], - hasSecondaryPiping: false, - secondaryPipingAmount: 0, - hasCoating: false, - coatingAmount: 0, - externalEquipmentCost: { - shippingCost: 500000, - highAltitudeWork: 0, - publicExpense: 2000000, - }, - specialNotes: '', - }, -}; - interface GetHandoverReportDetailResult { success: boolean; data?: HandoverReportDetail; error?: string; } +/** + * 인수인계보고서 상세 조회 + */ export async function getHandoverReportDetail(id: string): Promise { try { - // 실제 API 호출 시 여기에 구현 - // const response = await fetch(`/api/v1/handover-reports/${id}`); + const result = await apiRequest>(`/construction/handover-reports/${id}`); - const detail = MOCK_REPORT_DETAILS[id]; - - if (!detail) { - // 목록 데이터에서 기본 상세 생성 - const report = MOCK_REPORTS.find(r => r.id === id); - if (report) { - const generatedDetail: HandoverReportDetail = { - ...report, - contractDate: report.contractStartDate, - constructionPMId: 'pm1', - completionDate: null, - constructionManagers: [], - contractItems: [], - hasSecondaryPiping: false, - secondaryPipingAmount: 0, - hasCoating: false, - coatingAmount: 0, - externalEquipmentCost: { - shippingCost: 0, - highAltitudeWork: 0, - publicExpense: 0, - }, - specialNotes: '', - }; - return { - success: true, - data: generatedDetail, - }; - } - return { - success: false, - error: '인수인계보고서를 찾을 수 없습니다.', - }; + if (!result.success || !result.data) { + return { success: false, error: result.error || '인수인계보고서를 찾을 수 없습니다.' }; } - return { - success: true, - data: detail, - }; + return { success: true, data: transformHandoverReportDetail(result.data) }; } catch (error) { - console.error('Failed to fetch handover report detail:', error); - return { - success: false, - error: '인수인계보고서 상세 정보를 불러오는데 실패했습니다.', - }; + console.error('getHandoverReportDetail error:', error); + return { success: false, error: '인수인계보고서 상세 정보를 불러오는데 실패했습니다.' }; } } interface UpdateHandoverReportResult { success: boolean; + data?: HandoverReportDetail; error?: string; } +/** + * 인수인계보고서 수정 + */ export async function updateHandoverReport( id: string, data: HandoverReportFormData ): Promise { try { - // 실제 API 호출 시 여기에 구현 - console.log('Updating handover report:', id, data); + const apiData = transformToApiRequest(data); - return { - success: true, - }; + const result = await apiRequest>(`/construction/handover-reports/${id}`, { + method: 'PUT', + body: JSON.stringify(apiData), + }); + + if (!result.success || !result.data) { + return { success: false, error: result.error || '수정에 실패했습니다.' }; + } + + return { success: true, data: transformHandoverReportDetail(result.data) }; } catch (error) { - console.error('Failed to update handover report:', error); - return { - success: false, - error: '수정에 실패했습니다.', - }; + console.error('updateHandoverReport error:', error); + return { success: false, error: '수정에 실패했습니다.' }; } } + +interface CreateHandoverReportResult { + success: boolean; + data?: HandoverReportDetail; + error?: string; +} + +/** + * 인수인계보고서 등록 + */ +export async function createHandoverReport( + data: HandoverReportFormData +): Promise { + try { + const apiData = transformToApiRequest(data); + + const result = await apiRequest>('/construction/handover-reports', { + method: 'POST', + body: JSON.stringify(apiData), + }); + + if (!result.success || !result.data) { + return { success: false, error: result.error || '등록에 실패했습니다.' }; + } + + return { success: true, data: transformHandoverReportDetail(result.data) }; + } catch (error) { + console.error('createHandoverReport error:', error); + return { success: false, error: '등록에 실패했습니다.' }; + } +} \ No newline at end of file