- ImportService에 importIssuesToTask 메서드 추가
- ImportController에 importIssues 액션 추가
- ImportIssuesRequest FormRequest 생성
- POST /api/admin/pm/import/task/{taskId}/issues 라우트 추가
- import.blade.php UI에 '기존 작업에 이슈 추가' 모드 추가
- ImportProjectRequest에 tasks 레벨 검증 규칙 보완
632 lines
27 KiB
PHP
632 lines
27 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'JSON Import')
|
|
|
|
@section('content')
|
|
<div class="space-y-6">
|
|
{{-- 헤더 --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold text-gray-900">JSON Import</h1>
|
|
<p class="mt-1 text-sm text-gray-500">JSON 파일로 프로젝트, 작업, 이슈를 일괄 등록합니다.</p>
|
|
</div>
|
|
<a href="{{ route('pm.index') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
|
</svg>
|
|
대시보드로
|
|
</a>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
{{-- 왼쪽: Import 입력 --}}
|
|
<div class="space-y-6">
|
|
{{-- Import 모드 선택 --}}
|
|
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow-sm">
|
|
<h2 class="mb-4 text-lg font-medium text-gray-900">Import 모드</h2>
|
|
<div class="flex flex-wrap gap-4">
|
|
<label class="flex items-center">
|
|
<input type="radio" name="import_mode" value="new" checked
|
|
class="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
|
|
onchange="toggleImportMode()">
|
|
<span class="ml-2 text-sm text-gray-700">새 프로젝트 생성</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="radio" name="import_mode" value="existing"
|
|
class="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
|
|
onchange="toggleImportMode()">
|
|
<span class="ml-2 text-sm text-gray-700">기존 프로젝트에 작업 추가</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="radio" name="import_mode" value="task_issues"
|
|
class="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
|
|
onchange="toggleImportMode()">
|
|
<span class="ml-2 text-sm text-gray-700">기존 작업에 이슈 추가</span>
|
|
</label>
|
|
</div>
|
|
|
|
{{-- 기존 프로젝트 선택 (existing, task_issues 모드) --}}
|
|
<div id="existingProjectSelect" class="hidden mt-4">
|
|
<label class="block text-sm font-medium text-gray-700">대상 프로젝트</label>
|
|
<select id="targetProject" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
|
|
onchange="loadTasks()">
|
|
<option value="">프로젝트 선택...</option>
|
|
@foreach($projects as $project)
|
|
<option value="{{ $project->id }}">{{ $project->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- 작업 선택 (task_issues 모드) --}}
|
|
<div id="existingTaskSelect" class="hidden mt-4">
|
|
<label class="block text-sm font-medium text-gray-700">대상 작업</label>
|
|
<select id="targetTask" class="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
|
|
<option value="">작업 선택...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- JSON 입력 --}}
|
|
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow-sm">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-lg font-medium text-gray-900">JSON 입력</h2>
|
|
<div class="flex space-x-2">
|
|
<button type="button" onclick="loadTemplate()"
|
|
class="px-3 py-1.5 text-xs font-medium text-indigo-600 border border-indigo-300 rounded-lg hover:bg-indigo-50">
|
|
샘플 불러오기
|
|
</button>
|
|
<button type="button" onclick="formatJson()"
|
|
class="px-3 py-1.5 text-xs font-medium text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50">
|
|
포맷팅
|
|
</button>
|
|
<button type="button" onclick="clearJson()"
|
|
class="px-3 py-1.5 text-xs font-medium text-red-600 border border-red-300 rounded-lg hover:bg-red-50">
|
|
초기화
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<textarea id="jsonInput"
|
|
rows="20"
|
|
class="block w-full px-4 py-3 font-mono text-sm border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
|
|
placeholder='{ "project": { "name": "프로젝트명" }, "tasks": [...] }'></textarea>
|
|
<div id="jsonError" class="hidden mt-2 text-sm text-red-600"></div>
|
|
</div>
|
|
|
|
{{-- 파일 업로드 --}}
|
|
<div class="mt-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">또는 파일 업로드</label>
|
|
<input type="file" id="jsonFile" accept=".json"
|
|
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
|
|
onchange="loadFile(event)">
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 액션 버튼 --}}
|
|
<div class="flex space-x-3">
|
|
<button type="button" onclick="validateJson()"
|
|
class="flex-1 px-4 py-2.5 text-sm font-medium text-indigo-700 bg-indigo-100 rounded-lg hover:bg-indigo-200">
|
|
JSON 검증
|
|
</button>
|
|
<button type="button" onclick="importData()"
|
|
class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700">
|
|
Import 실행
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 오른쪽: 도움말 --}}
|
|
<div class="space-y-6">
|
|
{{-- 결과/상태 표시 --}}
|
|
<div id="resultCard" class="hidden p-6 bg-white border border-gray-200 rounded-lg shadow-sm">
|
|
<h2 class="mb-4 text-lg font-medium text-gray-900">결과</h2>
|
|
<div id="resultContent"></div>
|
|
</div>
|
|
|
|
{{-- JSON 구조 가이드 --}}
|
|
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow-sm">
|
|
<h2 class="mb-4 text-lg font-medium text-gray-900">JSON 구조 가이드</h2>
|
|
<div class="prose prose-sm max-w-none">
|
|
<h4 class="text-sm font-medium text-gray-900">새 프로젝트 생성</h4>
|
|
<pre class="p-3 mt-2 overflow-x-auto text-xs bg-gray-50 rounded-lg"><code>{
|
|
"project": {
|
|
"name": "SAM 시스템 개발",
|
|
"description": "프로젝트 설명입니다",
|
|
"status": "active",
|
|
"start_date": "2025-01-01",
|
|
"end_date": "2025-03-31"
|
|
},
|
|
"tasks": [
|
|
{
|
|
"title": "API 개발",
|
|
"description": "REST API 엔드포인트 구현",
|
|
"status": "in_progress",
|
|
"priority": "high",
|
|
"is_urgent": false,
|
|
"due_date": "2025-01-15",
|
|
"assignee_id": null,
|
|
"assignee_name": "김개발",
|
|
"issues": [
|
|
{
|
|
"title": "사용자 인증 구현",
|
|
"description": "JWT 기반 인증 시스템",
|
|
"type": "feature",
|
|
"status": "open",
|
|
"start_date": "2025-01-01",
|
|
"due_date": "2025-01-15",
|
|
"estimated_hours": 8,
|
|
"is_urgent": false,
|
|
"department_id": null,
|
|
"team": "개발팀",
|
|
"assignee_id": null,
|
|
"assignee_name": "홍길동",
|
|
"client": "내부"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}</code></pre>
|
|
|
|
<h4 class="mt-6 text-sm font-medium text-gray-900">기존 프로젝트에 작업 추가</h4>
|
|
<pre class="p-3 mt-2 overflow-x-auto text-xs bg-gray-50 rounded-lg"><code>{
|
|
"tasks": [
|
|
{
|
|
"title": "추가할 작업",
|
|
"priority": "high",
|
|
"is_urgent": true,
|
|
"issues": [...]
|
|
}
|
|
]
|
|
}</code></pre>
|
|
|
|
<h4 class="mt-6 text-sm font-medium text-gray-900">기존 작업에 이슈 추가</h4>
|
|
<pre class="p-3 mt-2 overflow-x-auto text-xs bg-gray-50 rounded-lg"><code>{
|
|
"issues": [
|
|
{
|
|
"title": "Phase 1: 견적관리",
|
|
"description": "공수: 1.75일",
|
|
"type": "feature",
|
|
"status": "open",
|
|
"start_date": "2024-11-27",
|
|
"due_date": "2024-11-28",
|
|
"estimated_hours": 14,
|
|
"team": "개발팀"
|
|
},
|
|
{
|
|
"title": "Phase 2: 기준정보-견적수식",
|
|
"description": "공수: 0.3일",
|
|
"type": "feature",
|
|
"status": "open",
|
|
"start_date": "2024-11-28",
|
|
"due_date": "2024-12-01"
|
|
}
|
|
]
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 필드 설명 --}}
|
|
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow-sm">
|
|
<h2 class="mb-4 text-lg font-medium text-gray-900">필드 설명</h2>
|
|
<div class="space-y-4 text-sm">
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">프로젝트 상태</h4>
|
|
<ul class="mt-1 ml-4 text-gray-600 list-disc">
|
|
<li><code class="px-1 bg-gray-100 rounded">active</code> - 진행중</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">completed</code> - 완료</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">on_hold</code> - 보류</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">작업 상태</h4>
|
|
<ul class="mt-1 ml-4 text-gray-600 list-disc">
|
|
<li><code class="px-1 bg-gray-100 rounded">todo</code> - 예정</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">in_progress</code> - 진행중</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">done</code> - 완료</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">우선순위</h4>
|
|
<ul class="mt-1 ml-4 text-gray-600 list-disc">
|
|
<li><code class="px-1 bg-gray-100 rounded">low</code> - 낮음</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">medium</code> - 보통</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">high</code> - 높음</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">이슈 타입</h4>
|
|
<ul class="mt-1 ml-4 text-gray-600 list-disc">
|
|
<li><code class="px-1 bg-gray-100 rounded">bug</code> - 버그</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">feature</code> - 기능</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">improvement</code> - 개선</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">이슈 상태</h4>
|
|
<ul class="mt-1 ml-4 text-gray-600 list-disc">
|
|
<li><code class="px-1 bg-gray-100 rounded">open</code> - 대기중</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">in_progress</code> - 처리중</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">resolved</code> - 해결됨</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">closed</code> - 종료</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">담당자/팀 (하이브리드)</h4>
|
|
<ul class="mt-1 ml-4 text-gray-600 list-disc">
|
|
<li><code class="px-1 bg-gray-100 rounded">department_id</code> - 부서 ID (DB 연동)</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">team</code> - 팀명 (문자열 직접 입력)</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">assignee_id</code> - 담당자 ID (DB 연동)</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">assignee_name</code> - 담당자명 (문자열)</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">client</code> - 고객사명</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-medium text-gray-900">기타 필드</h4>
|
|
<ul class="mt-1 ml-4 text-gray-600 list-disc">
|
|
<li><code class="px-1 bg-gray-100 rounded">is_urgent</code> - 긴급 여부 (true/false)</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">estimated_hours</code> - 예상 소요시간 (시간)</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">start_date</code> - 시작일 (YYYY-MM-DD)</li>
|
|
<li><code class="px-1 bg-gray-100 rounded">due_date</code> - 마감일 (YYYY-MM-DD)</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 알림 토스트 --}}
|
|
<div id="toast" class="fixed hidden px-6 py-3 text-white transition-opacity duration-300 transform -translate-x-1/2 rounded-lg shadow-lg bottom-6 left-1/2"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
// 샘플 템플릿
|
|
const sampleTemplate = @json($template);
|
|
|
|
// Import 모드 토글
|
|
function toggleImportMode() {
|
|
const mode = document.querySelector('input[name="import_mode"]:checked').value;
|
|
const existingSelect = document.getElementById('existingProjectSelect');
|
|
const taskSelect = document.getElementById('existingTaskSelect');
|
|
|
|
if (mode === 'existing' || mode === 'task_issues') {
|
|
existingSelect.classList.remove('hidden');
|
|
} else {
|
|
existingSelect.classList.add('hidden');
|
|
}
|
|
|
|
if (mode === 'task_issues') {
|
|
taskSelect.classList.remove('hidden');
|
|
loadTasks(); // 작업 목록 로드
|
|
} else {
|
|
taskSelect.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// 프로젝트별 작업 목록 로드
|
|
async function loadTasks() {
|
|
const mode = document.querySelector('input[name="import_mode"]:checked').value;
|
|
if (mode !== 'task_issues') return;
|
|
|
|
const projectId = document.getElementById('targetProject').value;
|
|
const taskSelect = document.getElementById('targetTask');
|
|
|
|
if (!projectId) {
|
|
taskSelect.innerHTML = '<option value="">작업 선택...</option>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/pm/tasks/project/${projectId}`, {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new Error('작업 목록 로드 실패');
|
|
|
|
const result = await response.json();
|
|
const tasks = result.data || [];
|
|
|
|
taskSelect.innerHTML = '<option value="">작업 선택...</option>';
|
|
tasks.forEach(task => {
|
|
const option = document.createElement('option');
|
|
option.value = task.id;
|
|
option.textContent = task.title;
|
|
taskSelect.appendChild(option);
|
|
});
|
|
} catch (e) {
|
|
showError('작업 목록 로드 실패: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// 샘플 템플릿 불러오기
|
|
function loadTemplate() {
|
|
const mode = document.querySelector('input[name="import_mode"]:checked').value;
|
|
let template = sampleTemplate;
|
|
|
|
if (mode === 'existing') {
|
|
// 기존 프로젝트 모드에서는 tasks만 표시
|
|
template = { tasks: sampleTemplate.tasks };
|
|
} else if (mode === 'task_issues') {
|
|
// 기존 작업에 이슈 추가 모드에서는 issues만 표시
|
|
template = {
|
|
issues: [
|
|
{
|
|
title: "이슈 제목",
|
|
description: "이슈 설명",
|
|
type: "feature",
|
|
status: "open",
|
|
start_date: new Date().toISOString().split('T')[0],
|
|
due_date: new Date(Date.now() + 7*24*60*60*1000).toISOString().split('T')[0],
|
|
estimated_hours: 8,
|
|
team: "개발팀"
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
document.getElementById('jsonInput').value = JSON.stringify(template, null, 2);
|
|
hideError();
|
|
}
|
|
|
|
// JSON 포맷팅
|
|
function formatJson() {
|
|
const input = document.getElementById('jsonInput');
|
|
try {
|
|
const json = JSON.parse(input.value);
|
|
input.value = JSON.stringify(json, null, 2);
|
|
hideError();
|
|
} catch (e) {
|
|
showError('JSON 파싱 오류: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// JSON 초기화
|
|
function clearJson() {
|
|
document.getElementById('jsonInput').value = '';
|
|
hideError();
|
|
hideResult();
|
|
}
|
|
|
|
// 파일 로드
|
|
function loadFile(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
try {
|
|
const json = JSON.parse(e.target.result);
|
|
document.getElementById('jsonInput').value = JSON.stringify(json, null, 2);
|
|
hideError();
|
|
} catch (err) {
|
|
showError('파일 파싱 오류: ' + err.message);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// JSON 검증
|
|
async function validateJson() {
|
|
const input = document.getElementById('jsonInput').value;
|
|
|
|
if (!input.trim()) {
|
|
showError('JSON을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(input);
|
|
} catch (e) {
|
|
showError('JSON 파싱 오류: ' + e.message);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/pm/import/validate', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
try {
|
|
const errorJson = JSON.parse(text);
|
|
showError('검증 실패: ' + (errorJson.message || '서버 오류'));
|
|
return;
|
|
} catch {
|
|
showError(`서버 오류 (${response.status}): ${text.substring(0, 200)}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showResult('success', 'JSON 구조가 유효합니다!');
|
|
hideError();
|
|
} else {
|
|
showResult('error', '검증 실패', result.errors || [result.message]);
|
|
}
|
|
} catch (e) {
|
|
showError('요청 실패: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Import 실행
|
|
async function importData() {
|
|
const input = document.getElementById('jsonInput').value;
|
|
const mode = document.querySelector('input[name="import_mode"]:checked').value;
|
|
|
|
if (!input.trim()) {
|
|
showError('JSON을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(input);
|
|
} catch (e) {
|
|
showError('JSON 파싱 오류: ' + e.message);
|
|
return;
|
|
}
|
|
|
|
let url = '/api/admin/pm/import';
|
|
if (mode === 'existing') {
|
|
const projectId = document.getElementById('targetProject').value;
|
|
if (!projectId) {
|
|
showError('대상 프로젝트를 선택해주세요.');
|
|
return;
|
|
}
|
|
url = `/api/admin/pm/import/project/${projectId}/tasks`;
|
|
} else if (mode === 'task_issues') {
|
|
const taskId = document.getElementById('targetTask').value;
|
|
if (!taskId) {
|
|
showError('대상 작업을 선택해주세요.');
|
|
return;
|
|
}
|
|
url = `/api/admin/pm/import/task/${taskId}/issues`;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
try {
|
|
const errorJson = JSON.parse(text);
|
|
showError('Import 실패: ' + (errorJson.message || '서버 오류'));
|
|
return;
|
|
} catch {
|
|
showError(`서버 오류 (${response.status}): ${text.substring(0, 200)}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
let resultItems = [];
|
|
if (mode === 'task_issues') {
|
|
resultItems = [
|
|
`작업: ${result.data.task_title}`,
|
|
`이슈: ${result.data.issues_count}개 추가`,
|
|
];
|
|
} else {
|
|
resultItems = [
|
|
`프로젝트: ${result.data.project_name || result.data.project_id}`,
|
|
`작업: ${result.data.tasks_count}개`,
|
|
`이슈: ${result.data.issues_count}개`,
|
|
];
|
|
}
|
|
showResult('success', result.message, resultItems);
|
|
showToast('Import 완료!', 'success');
|
|
|
|
// 새 프로젝트면 상세 페이지로 이동 버튼 표시
|
|
if (mode === 'new' && result.data.project_id) {
|
|
appendViewButton(result.data.project_id);
|
|
}
|
|
// 작업 이슈 추가 모드면 프로젝트 상세 페이지로 이동 버튼 표시
|
|
if (mode === 'task_issues' && result.data.project_id) {
|
|
appendViewButton(result.data.project_id);
|
|
}
|
|
} else {
|
|
showResult('error', 'Import 실패', [result.message]);
|
|
}
|
|
} catch (e) {
|
|
showError('요청 실패: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// 에러 표시
|
|
function showError(message) {
|
|
const errorDiv = document.getElementById('jsonError');
|
|
errorDiv.textContent = message;
|
|
errorDiv.classList.remove('hidden');
|
|
}
|
|
|
|
// 에러 숨기기
|
|
function hideError() {
|
|
document.getElementById('jsonError').classList.add('hidden');
|
|
}
|
|
|
|
// 결과 표시
|
|
function showResult(type, title, items = []) {
|
|
const card = document.getElementById('resultCard');
|
|
const content = document.getElementById('resultContent');
|
|
|
|
const bgColor = type === 'success' ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200';
|
|
const textColor = type === 'success' ? 'text-green-800' : 'text-red-800';
|
|
const iconPath = type === 'success'
|
|
? 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
|
|
|
|
let itemsHtml = items.length ? '<ul class="mt-2 ml-6 text-sm list-disc">' + items.map(i => `<li>${i}</li>`).join('') + '</ul>' : '';
|
|
|
|
content.innerHTML = `
|
|
<div class="p-4 border rounded-lg ${bgColor}">
|
|
<div class="flex items-center ${textColor}">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}"/>
|
|
</svg>
|
|
<span class="font-medium">${title}</span>
|
|
</div>
|
|
${itemsHtml}
|
|
<div id="viewButtonContainer"></div>
|
|
</div>
|
|
`;
|
|
|
|
card.classList.remove('hidden');
|
|
}
|
|
|
|
// 결과 숨기기
|
|
function hideResult() {
|
|
document.getElementById('resultCard').classList.add('hidden');
|
|
}
|
|
|
|
// 프로젝트 보기 버튼 추가
|
|
function appendViewButton(projectId) {
|
|
const container = document.getElementById('viewButtonContainer');
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<a href="/project-management/projects/${projectId}"
|
|
class="inline-flex items-center px-3 py-1.5 mt-3 text-sm font-medium text-green-700 bg-green-100 rounded-lg hover:bg-green-200">
|
|
프로젝트 보기 →
|
|
</a>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 토스트 표시
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.getElementById('toast');
|
|
const bgColor = type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-gray-800';
|
|
|
|
toast.className = `fixed px-6 py-3 text-white rounded-lg shadow-lg bottom-6 left-1/2 transform -translate-x-1/2 ${bgColor}`;
|
|
toast.textContent = message;
|
|
toast.classList.remove('hidden');
|
|
|
|
setTimeout(() => {
|
|
toast.classList.add('hidden');
|
|
}, 3000);
|
|
}
|
|
</script>
|
|
@endpush |