From eb99efb014ad1ddecd1f7b4154f1aecda63afd94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 20:23:50 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=ED=99=88=ED=83=9D=EC=8A=A4=20?= =?UTF-8?q?=EC=88=98=EB=8F=99=EC=9E=85=EB=A0=A5=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=EC=97=90=20=EA=B1=B0=EB=9E=98=EC=B2=98=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HometaxTradingPartnerSelect 컴포넌트 추가 (검색+키보드 탐색) - HometaxAddTradingPartnerModal 컴포넌트 추가 (신규 거래처 등록) - 거래처 선택 시 거래처명+사업자번호 자동채움 - 매출/매입 전환 시 거래처 선택 초기화 - 카드내역 불러오기와 거래처 드롭다운 동기화 - 수정 모달 시 기존 거래처명으로 드롭다운 매칭 Co-Authored-By: Claude Opus 4.6 --- .../views/barobill/hometax/index.blade.php | 304 +++++++++++++++++- 1 file changed, 302 insertions(+), 2 deletions(-) diff --git a/resources/views/barobill/hometax/index.blade.php b/resources/views/barobill/hometax/index.blade.php index 098c354a..4d93b038 100644 --- a/resources/views/barobill/hometax/index.blade.php +++ b/resources/views/barobill/hometax/index.blade.php @@ -61,7 +61,7 @@ + + + +@verbatim + +@endverbatim +@endpush diff --git a/routes/web.php b/routes/web.php index 665ce111..1cfbd881 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1222,4 +1222,30 @@ Route::post('/products', [\App\Http\Controllers\Sales\SalesContractController::class, 'saveProducts'])->name('products.save'); Route::get('/products/{tenant}', [\App\Http\Controllers\Sales\SalesContractController::class, 'getProducts'])->name('products.get'); }); + + // 인터뷰 시나리오 관리 + Route::prefix('interviews')->name('interviews.')->group(function () { + Route::get('/', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'index'])->name('index'); + // 카테고리 API + Route::get('/api/categories', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'categories'])->name('api.categories'); + Route::post('/api/categories', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'storeCategory'])->name('api.categories.store'); + Route::put('/api/categories/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'updateCategory'])->name('api.categories.update'); + Route::delete('/api/categories/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'destroyCategory'])->name('api.categories.destroy'); + // 트리 API + Route::get('/api/tree', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'tree'])->name('api.tree'); + // 템플릿(항목) API + Route::post('/api/templates', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'storeTemplate'])->name('api.templates.store'); + Route::put('/api/templates/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'updateTemplate'])->name('api.templates.update'); + Route::delete('/api/templates/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'destroyTemplate'])->name('api.templates.destroy'); + // 질문 API + Route::post('/api/questions', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'storeQuestion'])->name('api.questions.store'); + Route::put('/api/questions/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'updateQuestion'])->name('api.questions.update'); + Route::delete('/api/questions/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'destroyQuestion'])->name('api.questions.destroy'); + // 세션 API + Route::get('/api/sessions', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'sessions'])->name('api.sessions'); + Route::post('/api/sessions', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'storeSession'])->name('api.sessions.store'); + Route::get('/api/sessions/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'showSession'])->name('api.sessions.show'); + Route::post('/api/sessions/toggle-answer', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'toggleAnswer'])->name('api.sessions.toggle-answer'); + Route::post('/api/sessions/{id}/complete', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'completeSession'])->name('api.sessions.complete'); + }); }); From d0e4a8e6a28f7c6cee7da6b7464efdf19614d477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 21:09:02 +0900 Subject: [PATCH 4/9] =?UTF-8?q?fix:=EC=9D=B8=ED=84=B0=EB=B7=B0=20=EC=8B=9C?= =?UTF-8?q?=EB=82=98=EB=A6=AC=EC=98=A4=20=EB=A9=94=EB=89=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=ED=85=8C=EB=84=8C=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존: tenant_id=1에만 메뉴 생성 - 수정: 글로벌(null) + 모든 테넌트의 영업관리 하위에 생성 Co-Authored-By: Claude Opus 4.6 --- database/seeders/InterviewMenuSeeder.php | 72 ++++++++++++------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/database/seeders/InterviewMenuSeeder.php b/database/seeders/InterviewMenuSeeder.php index e5bae7fd..c468144e 100644 --- a/database/seeders/InterviewMenuSeeder.php +++ b/database/seeders/InterviewMenuSeeder.php @@ -9,49 +9,51 @@ class InterviewMenuSeeder extends Seeder { public function run(): void { - $tenantId = 1; - - // 영업관리 상위 메뉴 찾기 - $salesParentId = Menu::where('tenant_id', $tenantId) + // 모든 테넌트의 영업관리 메뉴 찾기 (글로벌 포함) + $salesMenus = Menu::withoutGlobalScopes() ->where('name', '영업관리') ->whereNull('parent_id') - ->value('id'); + ->whereNull('deleted_at') + ->get(); - if (!$salesParentId) { + if ($salesMenus->isEmpty()) { $this->command->error('영업관리 메뉴를 찾을 수 없습니다.'); return; } - // 이미 존재하는지 확인 - $exists = Menu::where('tenant_id', $tenantId) - ->where('parent_id', $salesParentId) - ->where('name', '인터뷰 시나리오') - ->exists(); + foreach ($salesMenus as $salesMenu) { + $tenantId = $salesMenu->tenant_id; + $label = $tenantId ? "tenant_id={$tenantId}" : 'global(tenant_id=null)'; - if ($exists) { - $this->command->info('인터뷰 시나리오 메뉴가 이미 존재합니다.'); - return; + // 이미 존재하는지 확인 + $exists = Menu::withoutGlobalScopes() + ->where('parent_id', $salesMenu->id) + ->where('name', '인터뷰 시나리오') + ->whereNull('deleted_at') + ->exists(); + + if ($exists) { + $this->command->info("[{$label}] 인터뷰 시나리오 메뉴가 이미 존재합니다."); + continue; + } + + // 마지막 sort_order 확인 + $maxSort = Menu::withoutGlobalScopes() + ->where('parent_id', $salesMenu->id) + ->whereNull('deleted_at') + ->max('sort_order') ?? 0; + + Menu::withoutGlobalScopes()->create([ + 'tenant_id' => $tenantId, + 'parent_id' => $salesMenu->id, + 'name' => '인터뷰 시나리오', + 'url' => '/sales/interviews', + 'icon' => 'clipboard-check', + 'sort_order' => $maxSort + 1, + 'is_active' => true, + ]); + + $this->command->info("[{$label}] 인터뷰 시나리오 메뉴 생성 완료."); } - - // 영업파트너 승인 메뉴의 sort_order 확인 - $approvalMenu = Menu::where('tenant_id', $tenantId) - ->where('parent_id', $salesParentId) - ->where('name', '영업파트너 승인') - ->first(); - - $sortOrder = $approvalMenu ? $approvalMenu->sort_order + 1 : 10; - - // 인터뷰 시나리오 메뉴 생성 - Menu::create([ - 'tenant_id' => $tenantId, - 'parent_id' => $salesParentId, - 'name' => '인터뷰 시나리오', - 'url' => '/sales/interviews', - 'icon' => 'clipboard-check', - 'sort_order' => $sortOrder, - 'is_active' => true, - ]); - - $this->command->info('인터뷰 시나리오 메뉴가 생성되었습니다.'); } } From 98a8dbcab1424582844fc300929ce051f74d1855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 21:19:34 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix:=EC=9D=B8=ED=84=B0=EB=B7=B0=20=EC=8B=9C?= =?UTF-8?q?=EB=82=98=EB=A6=AC=EC=98=A4=20UI=20=EB=B2=84=ED=8A=BC=EC=9D=84?= =?UTF-8?q?=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lucide 아이콘 의존 제거하여 버튼 가시성 개선 - 카테고리/항목/질문의 추가/수정/삭제 버튼을 텍스트로 변경 - 빈 상태에서도 카테고리 추가 버튼 명확하게 표시 Co-Authored-By: Claude Opus 4.6 --- .../views/sales/interviews/index.blade.php | 89 ++++++++----------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/resources/views/sales/interviews/index.blade.php b/resources/views/sales/interviews/index.blade.php index 671b178e..65f02b90 100644 --- a/resources/views/sales/interviews/index.blade.php +++ b/resources/views/sales/interviews/index.blade.php @@ -233,8 +233,9 @@ function CategorySidebar({ categories, selectedId, onSelect, onRefresh }) {
카테고리 -
@@ -254,9 +255,13 @@ className="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"> )}
- {categories.length === 0 && ( -
- 카테고리를 추가하세요 + {categories.length === 0 && !showAdd && ( +
+

카테고리가 없습니다

+
)} {categories.map(cat => ( @@ -268,24 +273,19 @@ className={`category-item px-4 py-2.5 cursor-pointer flex items-center justify-b setEditName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUpdate(cat.id); if (e.key === 'Escape') setEditingId(null); }} className="flex-1 text-sm border rounded px-1.5 py-0.5" autoFocus /> - - + +
) : ( <> {cat.name}
e.stopPropagation()}> - + className="text-xs text-blue-600 hover:text-blue-800">수정 +
)} @@ -306,7 +306,7 @@ function MainContent({ category, onRefresh }) { return (
- +

📋

카테고리를 선택하세요

@@ -343,8 +343,8 @@ function MainContent({ category, onRefresh }) { /> ) : ( )}
@@ -388,29 +388,22 @@ function TemplateCard({ template, onRefresh }) { setEditName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUpdate(); if (e.key === 'Escape') setEditing(false); }} className="flex-1 text-sm border rounded px-2 py-1" autoFocus /> - + + className="text-xs px-2 py-1 text-gray-500 hover:text-gray-700">취소
) : ( <>
- - {template.name} + 📄 {template.name} ({questions.length}개 질문)
-
+
- + className="text-xs text-blue-600 hover:text-blue-800">수정 +
)} @@ -433,8 +426,8 @@ className="text-gray-400 hover:text-blue-600 p-1"> /> ) : ( )}
@@ -473,29 +466,23 @@ function QuestionItem({ question, onRefresh }) { setEditText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUpdate(); if (e.key === 'Escape') setEditing(false); }} className="flex-1 text-sm border rounded px-2 py-1" autoFocus /> - + + className="text-xs px-1.5 py-0.5 text-gray-500 hover:text-gray-700">취소 ) : ( <>
- + {question.question_text} {question.is_required && *필수}
-
+
- + className="text-xs text-blue-600 hover:text-blue-800">수정 +
)} From 79f6fc29e8debf55d682518fa3e308703c389fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 21:22:02 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=ED=95=AD=EC=83=81=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hover 의존(hidden group-hover) 제거 - 선택된 카테고리에서 수정/삭제 버튼 항상 노출 Co-Authored-By: Claude Opus 4.6 --- .../views/sales/interviews/index.blade.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/resources/views/sales/interviews/index.blade.php b/resources/views/sales/interviews/index.blade.php index 65f02b90..eb44ba61 100644 --- a/resources/views/sales/interviews/index.blade.php +++ b/resources/views/sales/interviews/index.blade.php @@ -266,10 +266,10 @@ className="text-sm px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700" )} {categories.map(cat => (
onSelect(cat.id)}> {editingId === cat.id ? ( -
e.stopPropagation()}> +
e.stopPropagation()}> setEditName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUpdate(cat.id); if (e.key === 'Escape') setEditingId(null); }} className="flex-1 text-sm border rounded px-1.5 py-0.5" autoFocus /> @@ -279,15 +279,17 @@ className="text-xs px-1.5 py-0.5 bg-green-600 text-white rounded hover:bg-green- className="text-xs px-1.5 py-0.5 text-gray-500 hover:text-gray-700">취소
) : ( - <> +
{cat.name} -
e.stopPropagation()}> - - -
- + {selectedId === cat.id && ( +
e.stopPropagation()}> + + +
+ )} +
)}
))} From 32cbef9ae316c4dacf32b3469db44fcb7baddea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 21:42:14 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=EC=9D=B8=ED=84=B0=EB=B7=B0=20?= =?UTF-8?q?=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20MD=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=9D=BC=EA=B4=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../Sales/InterviewScenarioController.php | 19 ++++ .../Sales/InterviewScenarioService.php | 53 ++++++++++ .../views/sales/interviews/index.blade.php | 96 +++++++++++++++++++ routes/web.php | 2 + 4 files changed, 170 insertions(+) diff --git a/app/Http/Controllers/Sales/InterviewScenarioController.php b/app/Http/Controllers/Sales/InterviewScenarioController.php index c11436a2..d169ff55 100644 --- a/app/Http/Controllers/Sales/InterviewScenarioController.php +++ b/app/Http/Controllers/Sales/InterviewScenarioController.php @@ -151,6 +151,25 @@ public function destroyQuestion(int $id): JsonResponse return response()->json(['message' => '삭제되었습니다.']); } + // ============================================================ + // MD 파일 일괄 가져오기 + // ============================================================ + + public function bulkImport(Request $request): JsonResponse + { + $validated = $request->validate([ + 'category_id' => 'required|integer|exists:interview_categories,id', + 'templates' => 'required|array|min:1', + 'templates.*.name' => 'required|string|max:200', + 'templates.*.questions' => 'required|array|min:1', + 'templates.*.questions.*' => 'required|string|max:500', + ]); + + $result = $this->service->bulkImport($validated['category_id'], $validated['templates']); + + return response()->json($result, 201); + } + // ============================================================ // 세션 API // ============================================================ diff --git a/app/Services/Sales/InterviewScenarioService.php b/app/Services/Sales/InterviewScenarioService.php index 3ff8e819..6a35afb2 100644 --- a/app/Services/Sales/InterviewScenarioService.php +++ b/app/Services/Sales/InterviewScenarioService.php @@ -141,6 +141,59 @@ public function deleteQuestion(int $id): void $question->delete(); } + // ============================================================ + // MD 파일 일괄 가져오기 + // ============================================================ + + public function bulkImport(int $categoryId, array $templates): array + { + return DB::transaction(function () use ($categoryId, $templates) { + $tenantId = session('selected_tenant_id', 1); + $userId = auth()->id(); + $maxTemplateSort = InterviewTemplate::where('interview_category_id', $categoryId) + ->max('sort_order') ?? 0; + + $createdTemplates = 0; + $createdQuestions = 0; + + foreach ($templates as $tpl) { + $maxTemplateSort++; + $template = InterviewTemplate::create([ + 'tenant_id' => $tenantId, + 'interview_category_id' => $categoryId, + 'name' => $tpl['name'], + 'sort_order' => $maxTemplateSort, + 'is_active' => true, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + $createdTemplates++; + + $questionSort = 0; + foreach ($tpl['questions'] as $questionText) { + $questionSort++; + InterviewQuestion::create([ + 'tenant_id' => $tenantId, + 'interview_template_id' => $template->id, + 'question_text' => $questionText, + 'question_type' => 'checkbox', + 'is_required' => false, + 'sort_order' => $questionSort, + 'is_active' => true, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + $createdQuestions++; + } + } + + return [ + 'templates_created' => $createdTemplates, + 'questions_created' => $createdQuestions, + ]; + }); + } + // ============================================================ // 전체 트리 조회 // ============================================================ diff --git a/resources/views/sales/interviews/index.blade.php b/resources/views/sales/interviews/index.blade.php index eb44ba61..84011c7a 100644 --- a/resources/views/sales/interviews/index.blade.php +++ b/resources/views/sales/interviews/index.blade.php @@ -94,6 +94,7 @@ const IconBuilding = createIcon('building-2'); const IconFileText = createIcon('file-text'); const IconCheckCircle = createIcon('check-circle'); +const IconUpload = createIcon('upload'); // ============================================================ // 루트 앱 @@ -303,6 +304,60 @@ className="text-xs text-red-500 hover:text-red-700">삭제 // ============================================================ function MainContent({ category, onRefresh }) { const [showAddTemplate, setShowAddTemplate] = useState(false); + const [showMdPreview, setShowMdPreview] = useState(false); + const [mdParsed, setMdParsed] = useState([]); + const [mdImporting, setMdImporting] = useState(false); + const fileInputRef = useRef(null); + + const parseMd = (text) => { + const lines = text.split('\n'); + const result = []; + let current = null; + for (const line of lines) { + const headerMatch = line.match(/^#+\s+(.+)/); + if (headerMatch) { + current = { name: headerMatch[1].trim(), questions: [] }; + result.push(current); + continue; + } + const listMatch = line.match(/^[-*]\s+(.+)/); + if (listMatch && current) { + current.questions.push(listMatch[1].trim()); + } + } + return result.filter(t => t.questions.length > 0); + }; + + const handleFileSelect = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const parsed = parseMd(ev.target.result); + if (parsed.length === 0) { + alert('파싱 가능한 항목이 없습니다.\n# 헤더와 - 질문 형식을 확인하세요.'); + return; + } + setMdParsed(parsed); + setShowMdPreview(true); + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleBulkImport = async () => { + setMdImporting(true); + try { + await api.post('/sales/interviews/api/bulk-import', { + category_id: category.id, + templates: mdParsed, + }); + setShowMdPreview(false); + setMdParsed([]); + onRefresh(); + } catch (e) { alert('일괄 생성 실패: ' + e.message); } + finally { setMdImporting(false); } + }; if (!category) { return ( @@ -316,6 +371,7 @@ function MainContent({ category, onRefresh }) { } const templates = category.templates || []; + const totalMdQuestions = mdParsed.reduce((sum, t) => sum + t.questions.length, 0); return (
@@ -324,8 +380,48 @@ function MainContent({ category, onRefresh }) { {category.name} {templates.length}개 항목
+
+ + +
+ {/* MD 미리보기 */} + {showMdPreview && ( +
+
+ + MD 파싱 결과: 항목 {mdParsed.length}개, 질문 {totalMdQuestions}개 + + +
+
+ {mdParsed.map((tpl, i) => ( +
+
📄 {tpl.name}
+
    + {tpl.questions.map((q, j) => ( +
  • • {q}
  • + ))} +
+
+ ))} +
+
+ + +
+
+ )} +
{templates.map(tpl => ( diff --git a/routes/web.php b/routes/web.php index 1cfbd881..d9b165cd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1241,6 +1241,8 @@ Route::post('/api/questions', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'storeQuestion'])->name('api.questions.store'); Route::put('/api/questions/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'updateQuestion'])->name('api.questions.update'); Route::delete('/api/questions/{id}', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'destroyQuestion'])->name('api.questions.destroy'); + // 일괄 가져오기 API + Route::post('/api/bulk-import', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'bulkImport'])->name('api.bulk-import'); // 세션 API Route::get('/api/sessions', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'sessions'])->name('api.sessions'); Route::post('/api/sessions', [\App\Http\Controllers\Sales\InterviewScenarioController::class, 'storeSession'])->name('api.sessions.store'); From 57b532a69a2be4cbb391e850a865eed150dffd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 22:09:09 +0900 Subject: [PATCH 8/9] =?UTF-8?q?docs:=EB=B0=A9=ED=99=94=EC=85=94=ED=84=B0?= =?UTF-8?q?=20=EA=B2=AC=EC=A0=81=EA=B5=AC=EC=A1=B0=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EB=B7=B0=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20=EC=83=98?= =?UTF-8?q?=ED=94=8C=20MD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- docs/samples/방화셔터_견적구조_인터뷰.md | 87 ++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/samples/방화셔터_견적구조_인터뷰.md diff --git a/docs/samples/방화셔터_견적구조_인터뷰.md b/docs/samples/방화셔터_견적구조_인터뷰.md new file mode 100644 index 00000000..45a6f9f7 --- /dev/null +++ b/docs/samples/방화셔터_견적구조_인터뷰.md @@ -0,0 +1,87 @@ +# 견적 산출 엑셀 구조 검토 +- 견적 산출 엑셀 파일을 분석했는가? +- 엑셀 구조를 자세히 검토했는가? +- 이미 완성된 엑셀을 그대로 활용할 수 있는가? +- 견적 구조를 문서화하여 설명할 수 있는가? + +# 셔터 카테고리 및 제품 분류 +- 통합 견적(견적 산출 + 발주서 작성) 범위를 확인했는가? +- 스크린 셔터와 철제 셔터의 분류 기준을 확인했는가? +- 스크린 본체/철제 슬랫 본체에 따른 제품 구분을 이해했는가? +- 산업용 등 카테고리별 구분 체계를 확인했는가? + +# 셔터 제품명 및 구분 구조 +- KSS01, KSS02 등 제품명 체계를 파악했는가? +- KWW WS 등 대형 제품 포함 여부를 확인했는가? +- KQTS, KT201 등 전체 제품 목록을 확보했는가? +- 벽면형, 측면형, 혼합형 등 설치 유형별 분류를 확인했는가? + +# 셔터 본체 구성 +- 본체가 스크린 또는 철제 슬랫으로 구성됨을 확인했는가? +- 본체와 모터가 공통 구성 요소임을 확인했는가? +- 스크린 본체의 원단 종류(실리카, 와이어, 하이바)를 확인했는가? +- 스크린의 절단, 미싱, 포장 공정을 파악했는가? +- 환봉이 부자재 창고에서 별도 관리됨을 확인했는가? + +# 전동 개폐기(모터) 구성 +- 전동 개폐기(모터)의 품질심사 용어와 현장 용어 차이를 인지했는가? +- 모터 내부 구성품(절연 거품, 부자재)을 확인했는가? +- 모터 용량 범위(150kg~1,500kg)를 파악했는가? +- 인증 제품의 경우 모터 구성이 고정됨을 확인했는가? +- 모터를 별도 구매하여 부품으로만 관리하는 현재 방식을 확인했는가? + +# 인정제품 vs 비인정제품 구분 +- 인정제품은 모든 구성 요소가 고정된 완전 세트임을 확인했는가? +- 비인정제품은 본체만/모터만/부자재만 개별 주문 가능함을 확인했는가? +- 인증 제품의 고정 구성 목록을 확보했는가? + +# 연동 제어기 및 부자재 +- 연동 제어기의 노출형/매립형 구분을 확인했는가? +- 매립형 연동 제어기에 뒷박스 부자재가 추가됨을 확인했는가? +- 공임과 부자재가 견적의 핵심 항목임을 인지했는가? + +# 가이드레일 및 케이스 구성 +- 가이드레일이 제품마다 모양과 치수가 다름을 확인했는가? +- 가이드레일 표준 길이(2,438mm, 3,305mm, 4,430mm) 조합을 파악했는가? +- 케이스 크기별(1500×380, 500×380) 부속품 차이를 확인했는가? +- 가이드레일은 항상 2개씩 필요함을 확인했는가? +- 판재 절곡 방식에 따른 모양 차이를 이해했는가? + +# 오픈 사이즈와 제작 사이즈 +- 오픈 사이즈 입력에 따른 제작 사이즈 산출 방식을 확인했는가? +- 케이스/가이드레일 길이가 오픈 사이즈 확정 후 산출됨을 이해했는가? +- 현장 조합 및 용접 방식을 파악했는가? +- 표준 길이 판재의 현장 절단 방식을 확인했는가? + +# BOM 구조 현황 및 문제점 +- 현재 상향식(최하위 부품→상위 조립품) BOM 구조를 파악했는가? +- 제품별 부품 매칭 정보가 미구현 상태임을 확인했는가? +- KSS02 선택 시 부품 목록 자동 나열이 안 되는 문제를 인지했는가? +- 오픈 사이즈 기반 동적 부품 계산이 미구현임을 확인했는가? +- 케이스 내 부속품 연결이 표시되지 않는 문제를 확인했는가? + +# BOM 구조 개선 방안 +- 하향식 BOM 구조(상위 제품→하위 구성품) 도입을 검토했는가? +- KSS02 등록 시 하위 항목 자동 등록 플로우를 설계했는가? +- 카테고리별 품목 분류(본체, 절곡품, 부자재) 체계를 수립했는가? +- 제품-부품 매핑 테이블(기본 구성품/옵션 구성품)을 정의했는가? +- 오픈 사이즈 입력 시 동적 계산 부품 구분을 설계했는가? + +# 품목 등록 시스템 개선 +- 품목 등록 시 상위 품목 선택 기능을 구현할 수 있는가? +- 카테고리 선택 시 해당 목록만 표시하는 기능을 설계했는가? +- 제품별 부품 설정 등록 기능을 기획했는가? +- 상위 부품 선택 시 하위 부품 자동 표시를 구현할 수 있는가? + +# 제품 목록 및 데이터 확보 +- 5130 시스템의 제품 수(20개 미만)를 확인했는가? +- 미등록 제품이 더 있을 가능성을 검토했는가? +- 전체 제품(약 11개 + 스모크 관련) 목록을 확보했는가? +- 참고 자료 기반 부품 정리가 완료되었는가? + +# 견적 구조 확장 계획 +- 기본 복에서 제품 단위로 견적 확장 계획을 수립했는가? +- 제품별 원단 확정(예: KSS02→실리카) 정보를 정리했는가? +- 케이스 부품 종류 확정 및 사이즈/길이의 동적 입력 방식을 설계했는가? +- 공정 관리와 품목 연동 구조를 검토했는가? +- 개발팀과의 추가 협의 일정을 확인했는가? From 655dfc66411eac022dea4bc44566f80ef358a648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Feb 2026 22:15:02 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=EC=A7=88=EB=AC=B8=20=ED=96=89=20hover?= =?UTF-8?q?=20=EC=8B=9C=20=EB=AC=B8=EC=9E=A5=20=EB=81=9D=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=83=89=20X=20=EC=82=AD=EC=A0=9C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- resources/views/sales/interviews/index.blade.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/views/sales/interviews/index.blade.php b/resources/views/sales/interviews/index.blade.php index 84011c7a..295b7fcc 100644 --- a/resources/views/sales/interviews/index.blade.php +++ b/resources/views/sales/interviews/index.blade.php @@ -571,16 +571,18 @@ className="text-xs px-1.5 py-0.5 text-gray-500 hover:text-gray-700">취소 ) : ( <> -
+
{question.question_text} {question.is_required && *필수} +
-
+
-
)}