diff --git a/.DS_Store b/.DS_Store index 94e599e1..4cfd3d52 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.serena/.DS_Store b/.serena/.DS_Store new file mode 100644 index 00000000..eec887f2 Binary files /dev/null and b/.serena/.DS_Store differ diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 00000000..14d86ad6 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/memories/quote-registration-formfield-fix.md b/.serena/memories/quote-registration-formfield-fix.md new file mode 100644 index 00000000..308d2506 --- /dev/null +++ b/.serena/memories/quote-registration-formfield-fix.md @@ -0,0 +1,81 @@ +# 견적 등록/수정 FormField type="custom" 수정 작업 + +## 📅 작업일: 2026-01-06 + +## 🎯 문제 요약 +견적 등록/수정 페이지에서 **수량(quantity) 변경이 총합계에 반영되지 않는 버그** + +### 증상 +- 수량 1 → 자동견적산출 → 합계 1,711,225원 +- 수량 3으로 변경 → 자동견적산출 → 합계가 여전히 1,711,225원 (3배인 ~5,133,675원이어야 함) + +## 🔍 근본 원인 +**FormField 컴포넌트의 `type="custom"` 누락** + +FormField 컴포넌트 (`src/components/molecules/FormField.tsx`)는 `type` prop에 따라 다르게 동작: +- `type="custom"` → children(자식 요소)을 렌더링 +- 그 외 → 자체 내부 Input을 렌더링 (value/onChange 연결 안됨) + +```tsx +// FormField.tsx 내부 renderInput() 함수 +case 'custom': + return children; // ← children 렌더링 +default: + return // ← 자체 Input 렌더링 (value=undefined) +``` + +**결과**: `type="custom"` 없이 FormField 안에 Input을 넣으면, 해당 Input은 렌더링되지 않고 FormField 자체 Input이 렌더링됨 → state와 연결 끊김 + +## ✅ 수정 완료 (8개 FormField) + +### 파일: `src/components/quotes/QuoteRegistration.tsx` + +**기본정보 섹션** (3개): +1. 등록일 (line 581) - `type="custom"` 추가 +2. 현장명 (line 627) - `type="custom"` 추가 (datalist 자동완성 포함) +3. 납기일 (line 662) - `type="custom"` 추가 + +**견적 항목 섹션** (5개): +4. 층수 (line 733) - `type="custom"` 추가 +5. 부호 (line 744) - `type="custom"` 추가 +6. **수량 (line 926)** - `type="custom"` 추가 ⭐ 핵심 버그 원인 +7. 마구리 날개치수 (line 944) - `type="custom"` 추가 +8. 검사비 (line 959) - `type="custom"` 추가 + +## 추가 수정사항 + +### 1. useMemo로 calculatedGrandTotal 추가 (line 194-201) +```tsx +const calculatedGrandTotal = useMemo(() => { + if (!calculationResults?.items) return 0; + return calculationResults.items.reduce((sum, itemResult) => { + const formItem = formData.items[itemResult.index]; + return sum + (itemResult.result.grand_total * (formItem?.quantity || 1)); + }, 0); +}, [calculationResults, formData.items]); +``` + +### 2. Toast 메시지 수정 (line 493-498) +`updatedItems` 사용하여 최신 상태 반영 + +### 3. Badge 및 하단 총합계 +`calculatedGrandTotal` 사용 (line 1019, 1131) + +## ⚠️ 남은 이슈 +사용자가 "견적 산출 결과에는 왜 반영이 안되는거지?"라고 질문 → 확인 필요: +1. 수정된 코드로 테스트했는지 (브라우저 새로고침) +2. 수량 변경 후 즉시 반영되는지 vs 버튼 클릭 필요한지 +3. 현재 코드에서 수량 변경 시 합계는 실시간 업데이트되어야 함 (useMemo + React 재렌더링) + +## 📁 관련 파일 +- `src/components/quotes/QuoteRegistration.tsx` - 메인 수정 파일 +- `src/components/molecules/FormField.tsx` - FormField 컴포넌트 (참조) +- `src/app/[locale]/(protected)/sales/quote-management/[id]/edit/page.tsx` - 수정 페이지 + +## 🔑 핵심 교훈 +**FormField에 커스텀 children(Input, Select, datalist 등)을 넣을 때는 반드시 `type="custom"` 필요!** + +## 🚀 새 세션에서 이어서 작업하려면 +1. 프로젝트 활성화: Serena `activate_project` → "react" +2. 메모리 읽기: `read_memory("quote-registration-formfield-fix.md")` +3. 확인 필요: 수량 변경 시 견적 산출 결과가 실시간으로 업데이트되는지 테스트 diff --git a/.serena/memories/receivables-dynamic-months-fix.md b/.serena/memories/receivables-dynamic-months-fix.md new file mode 100644 index 00000000..036208ed --- /dev/null +++ b/.serena/memories/receivables-dynamic-months-fix.md @@ -0,0 +1,70 @@ +# 채권현황 동적월 지원 및 year=0 파라미터 버그 수정 + +## 작업 일시 +2026-01-02 + +## 문제 상황 +"최근 1년" 필터가 제대로 동작하지 않는 3가지 버그: +1. 2026년 조회 후 "최근 1년" 선택 시 2026년 기준 데이터 표시 +2. 2025년 조회 후 "최근 1년" 선택 시 2025년 기준 데이터 표시 +3. 초기 페이지 로드 시 "최근 1년" 기본값인데 데이터 없음 + +## 원인 분석 + +### 프론트엔드 (이전 세션에서 수정됨) +- JavaScript에서 `year === 0` 체크가 falsy 값 문제로 제대로 동작하지 않음 +- `if (year)` 같은 조건문에서 0이 false로 처리됨 + +### 백엔드 (이번 세션에서 수정) +- Laravel의 `'nullable|boolean'` 검증이 쿼리 파라미터로 전달된 문자열 "true"를 거부 +- HTTP 쿼리 파라미터는 항상 문자열로 전달됨 + +## 수정 내용 + +### 1. ReceivablesController.php +```php +// 변경 전 +'recent_year' => 'nullable|boolean', + +// 변경 후 +'recent_year' => 'nullable|string|in:true,false,1,0', + +// 검증 후 boolean 변환 +if (isset($params['recent_year'])) { + $params['recent_year'] = filter_var($params['recent_year'], FILTER_VALIDATE_BOOLEAN); +} + +\Log::info('[Receivables] index params', $params); +``` + +### 2. actions.ts (이전 세션 수정, 검증됨) +```typescript +const yearValue = params?.year; +if (typeof yearValue === 'number') { + if (yearValue === 0) { + searchParams.set('recent_year', 'true'); + } else { + searchParams.set('year', String(yearValue)); + } +} +``` + +## 핵심 포인트 +1. **명시적 타입 체크**: `typeof yearValue === 'number'`로 undefined와 0을 구분 +2. **문자열 boolean 검증**: Laravel에서 `'in:true,false,1,0'` 사용 후 `filter_var()` 변환 +3. **디버그 로깅**: 개발 중 파라미터 확인을 위한 로그 추가 (테스트 후 제거 필요) + +## Git 커밋 +- API: `4fa38e3` - feat(API): 채권현황 동적월 지원 및 year=0 파라미터 버그 수정 +- React: `672b1b4` - feat(WEB): 채권현황 동적월 지원 및 year=0 파라미터 버그 수정 +- React: `1f32b04` - docs: 채권현황 동적월 지원 작업 현황 업데이트 + +## 관련 파일 +- `/api/app/Http/Controllers/Api/V1/ReceivablesController.php` +- `/api/app/Services/ReceivablesService.php` +- `/react/src/components/accounting/ReceivablesStatus/actions.ts` +- `/react/src/components/accounting/ReceivablesStatus/index.tsx` + +## 후속 작업 +- [ ] 테스트 완료 후 디버그 로그 제거 +- [ ] 추가 UI 개선 (사용자 확인 필요) diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..aa57d260 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,84 @@ +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp csharp_omnisharp +# dart elixir elm erlang fortran go +# haskell java julia kotlin lua markdown +# nix perl php python python_jedi r +# rego ruby ruby_solargraph rust scala swift +# terraform typescript typescript_vts yaml zig +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# Special requirements: +# - csharp: Requires the presence of a .sln file in the project folder. +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- typescript + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed) on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "react" +included_optional_tools: [] diff --git a/claudedocs/.DS_Store b/claudedocs/.DS_Store index c8745c09..15466cfd 100644 Binary files a/claudedocs/.DS_Store and b/claudedocs/.DS_Store differ diff --git a/claudedocs/sales/[IMPL-2025-12-04] client-management-api-integration.md b/claudedocs/sales/[IMPL-2025-12-04] client-management-api-integration.md index 1350c9c7..30e09b51 100644 --- a/claudedocs/sales/[IMPL-2025-12-04] client-management-api-integration.md +++ b/claudedocs/sales/[IMPL-2025-12-04] client-management-api-integration.md @@ -2,7 +2,7 @@ > **작성일**: 2025-12-04 > **목적**: 거래처관리 페이지 API 연동 및 sam-design 기준 UI 구현 -ㅇ> **최종 업데이트**: 2025-12-04 ✅ 구현 완료 +> **최종 업데이트**: 2025-12-04 ✅ 구현 완료 --- diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..6ed4fef4 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# +# SAM React 배포 스크립트 +# 사용법: ./deploy.sh [dev|prod] +# + +set -e # 에러 발생 시 중단 + +# =========================================== +# 설정 +# =========================================== +ENV="${1:-dev}" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BUILD_FILE="next-build.tar.gz" + +# 개발 서버 설정 +DEV_SSH="hskwon@114.203.209.83" +DEV_PATH="/home/webservice/react" +DEV_PM2="sam-react" + +# 운영 서버 설정 (추후 설정) +# PROD_SSH="user@prod-server" +# PROD_PATH="/var/www/react" +# PROD_PM2="sam-react-prod" + +# 환경별 설정 선택 +case $ENV in + dev) + SSH_TARGET=$DEV_SSH + REMOTE_PATH=$DEV_PATH + PM2_APP=$DEV_PM2 + ;; + prod) + echo "❌ 운영 환경은 아직 설정되지 않았습니다." + exit 1 + ;; + *) + echo "❌ 알 수 없는 환경: $ENV" + echo "사용법: ./deploy.sh [dev|prod]" + exit 1 + ;; +esac + +# =========================================== +# 함수 정의 +# =========================================== +log() { + echo "" + echo "==========================================" + echo "🚀 $1" + echo "==========================================" +} + +error() { + echo "" + echo "❌ 에러: $1" + exit 1 +} + +# =========================================== +# 1. 빌드 +# =========================================== +log "Step 1/5: 빌드 시작" + +# .env.local 백업 +if [ -f .env.local ]; then + echo "📦 .env.local 백업..." + mv .env.local .env.local.bak +fi + +# 빌드 실행 +echo "🔨 npm run build..." +npm run build || { + # 빌드 실패 시 .env.local 복원 + if [ -f .env.local.bak ]; then + mv .env.local.bak .env.local + fi + error "빌드 실패" +} + +# .env.local 복원 +if [ -f .env.local.bak ]; then + echo "📦 .env.local 복원..." + mv .env.local.bak .env.local +fi + +echo "✅ 빌드 완료" + +# =========================================== +# 2. 압축 +# =========================================== +log "Step 2/5: 압축 시작" + +# 기존 압축 파일 삭제 +rm -f $BUILD_FILE + +# .next 폴더 압축 (캐시 제외) +echo "📦 .next 폴더 압축 중..." +COPYFILE_DISABLE=1 tar --exclude='.next/cache' -czf $BUILD_FILE .next + +# 파일 크기 확인 +FILE_SIZE=$(ls -lh $BUILD_FILE | awk '{print $5}') +echo "✅ 압축 완료: $BUILD_FILE ($FILE_SIZE)" + +# =========================================== +# 3. 업로드 +# =========================================== +log "Step 3/5: 서버 업로드" + +echo "📤 $SSH_TARGET:$REMOTE_PATH 로 업로드 중..." +scp $BUILD_FILE $SSH_TARGET:$REMOTE_PATH/ + +echo "✅ 업로드 완료" + +# =========================================== +# 4. 원격 배포 실행 +# =========================================== +log "Step 4/5: 원격 배포 실행" + +echo "🔧 서버에서 배포 스크립트 실행 중..." +ssh $SSH_TARGET << EOF + cd $REMOTE_PATH + + echo "🗑️ 기존 .next 폴더 삭제..." + rm -rf .next + + echo "📦 압축 해제 중..." + tar xzf $BUILD_FILE + + echo "🔄 PM2 재시작..." + pm2 restart $PM2_APP + + echo "🧹 압축 파일 정리..." + rm -f $BUILD_FILE + + echo "✅ 서버 배포 완료" +EOF + +# =========================================== +# 5. 정리 +# =========================================== +log "Step 5/5: 로컬 정리" + +echo "🧹 로컬 압축 파일 삭제..." +rm -f $BUILD_FILE + +# =========================================== +# 완료 +# =========================================== +echo "" +echo "==========================================" +echo "🎉 배포 완료!" +echo "==========================================" +echo "환경: $ENV" +echo "서버: $SSH_TARGET" +echo "경로: $REMOTE_PATH" +echo "시간: $(date '+%Y-%m-%d %H:%M:%S')" +echo "==========================================" \ No newline at end of file diff --git a/next-build_0113.tar.gz b/next-build_0113.tar.gz new file mode 100644 index 00000000..96988077 Binary files /dev/null and b/next-build_0113.tar.gz differ diff --git a/package-lock.json b/package-lock.json index fb9fcfed..8d685752 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7629,6 +7629,17 @@ } } }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..2d470d5e Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/components/accounting/VendorManagement/VendorDetail.tsx b/src/components/accounting/VendorManagement/VendorDetail.tsx index d710a99a..6b87909f 100644 --- a/src/components/accounting/VendorManagement/VendorDetail.tsx +++ b/src/components/accounting/VendorManagement/VendorDetail.tsx @@ -302,7 +302,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) { > 삭제 -ㄷ diff --git a/src/components/accounting/VendorManagement/index.tsx b/src/components/accounting/VendorManagement/index.tsx index 956dc7cc..c5520f52 100644 --- a/src/components/accounting/VendorManagement/index.tsx +++ b/src/components/accounting/VendorManagement/index.tsx @@ -22,6 +22,13 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { TableRow, TableCell } from '@/components/ui/table'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { IntegratedListTemplateV2, type TableColumn, @@ -456,6 +463,86 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement setCurrentPage(1); }, []); + // ===== 테이블 헤더 필터 액션 ===== + const tableHeaderActions = ( +
+ {/* 구분 필터 */} + + + {/* 신용등급 필터 */} + + + {/* 거래등급 필터 */} + + + {/* 악성채권 필터 */} + + + {/* 정렬 필터 */} + + + {/* 필터 초기화 버튼 */} + +
+ ); + return ( <> ([]); + + // 공과 품목 옵션 조회 + useEffect(() => { + async function fetchExpenseOptions() { + const result = await getExpenseItemOptions(); + if (result.success && result.data) { + setExpenseOptions(result.data); + } + } + fetchExpenseOptions(); + }, []); + // 적용된 조정단가 (전체 적용 버튼 클릭 시 복사됨) const [appliedPrices, setAppliedPrices] = useState<{ caulking: number; @@ -202,7 +217,7 @@ export default function EstimateDetailForm({ const handleAddExpenseItems = useCallback((count: number) => { const newItems = Array.from({ length: count }, () => ({ id: String(Date.now() + Math.random()), - name: MOCK_EXPENSES[0]?.value || '', + name: expenseOptions[0]?.value || '', amount: 100000, selected: false, })); @@ -210,7 +225,7 @@ export default function EstimateDetailForm({ ...prev, expenseItems: [...prev.expenseItems, ...newItems], })); - }, []); + }, [expenseOptions]); const handleRemoveSelectedExpenseItems = useCallback(() => { const selectedIds = formData.expenseItems @@ -627,6 +642,7 @@ export default function EstimateDetailForm({ {/* 공과 상세 */} ({ value: opt.value, label: opt.label }))} isViewMode={isViewMode} onAddItems={handleAddExpenseItems} onRemoveSelected={handleRemoveSelectedExpenseItems} diff --git a/src/components/business/construction/estimates/actions.ts b/src/components/business/construction/estimates/actions.ts index 57d0c525..ab5eddb1 100644 --- a/src/components/business/construction/estimates/actions.ts +++ b/src/components/business/construction/estimates/actions.ts @@ -639,4 +639,62 @@ export async function deleteEstimates(ids: string[]): Promise<{ console.error('견적 일괄 삭제 오류:', error); return { success: false, error: '일괄 삭제에 실패했습니다.' }; } +} + +// ======================================== +// 공과 품목 조회 (Items API) +// ======================================== + +/** + * 공과 품목 옵션 + */ +export interface ExpenseItemOption { + value: string; // 품목 ID + label: string; // 품목명 + code: string; // 품목코드 + unit: string; // 단위 +} + +/** + * 공과 품목 목록 조회 + * GET /api/v1/items?type=RM + * 품목관리에서 item_type='RM' (공과) 인 품목만 조회 + */ +export async function getExpenseItemOptions(): Promise<{ + success: boolean; + data?: ExpenseItemOption[]; + error?: string; +}> { + try { + const response = await apiClient.get<{ + data: Array<{ + id: number; + code: string; + name: string; + unit: string | null; + item_type: string; + is_active: boolean; + }>; + meta?: { total: number }; + }>('/items', { + params: { + item_type: 'RM', // 공과 품목만 조회 + active: '1', // 활성 품목만 + size: '100', // 충분한 수량 + }, + }); + + const items = Array.isArray(response.data) ? response.data : []; + const options: ExpenseItemOption[] = items.map((item) => ({ + value: String(item.id), + label: item.name, + code: item.code, + unit: item.unit || 'EA', + })); + + return { success: true, data: options }; + } catch (error) { + console.error('공과 품목 목록 조회 오류:', error); + return { success: false, error: '공과 품목 목록을 불러오는데 실패했습니다.' }; + } } \ No newline at end of file diff --git a/src/components/business/construction/estimates/sections/ExpenseDetailSection.tsx b/src/components/business/construction/estimates/sections/ExpenseDetailSection.tsx index d4459037..246d96ff 100644 --- a/src/components/business/construction/estimates/sections/ExpenseDetailSection.tsx +++ b/src/components/business/construction/estimates/sections/ExpenseDetailSection.tsx @@ -20,10 +20,17 @@ import { TableRow, } from '@/components/ui/table'; import type { ExpenseItem } from '../types'; -import { formatAmount, MOCK_EXPENSES } from '../utils'; +import { formatAmount } from '../utils'; + +// 공과 옵션 타입 +interface ExpenseOption { + value: string; + label: string; +} interface ExpenseDetailSectionProps { expenseItems: ExpenseItem[]; + expenseOptions: ExpenseOption[]; // 공과 품목 옵션 (Items API에서 조회) isViewMode: boolean; onAddItems: (count: number) => void; onRemoveSelected: () => void; @@ -34,6 +41,7 @@ interface ExpenseDetailSectionProps { export function ExpenseDetailSection({ expenseItems, + expenseOptions, isViewMode, onAddItems, onRemoveSelected, @@ -142,11 +150,17 @@ export function ExpenseDetailSection({ - {MOCK_EXPENSES.map((option) => ( - - {option.label} + {expenseOptions.length === 0 ? ( + + 등록된 공과 품목이 없습니다 - ))} + ) : ( + expenseOptions.map((option) => ( + + {option.label} + + )) + )} diff --git a/src/components/business/construction/estimates/utils/constants.ts b/src/components/business/construction/estimates/utils/constants.ts index efe41b6e..ac9fc236 100644 --- a/src/components/business/construction/estimates/utils/constants.ts +++ b/src/components/business/construction/estimates/utils/constants.ts @@ -6,8 +6,5 @@ export const MOCK_MATERIALS = [ { value: 'jointbar', label: '조인트바' }, ]; -// 목업 공과 목록 -export const MOCK_EXPENSES = [ - { value: 'public_1', label: '공과비 V' }, - { value: 'public_2', label: '공과비 A' }, -]; \ No newline at end of file +// 공과 품목은 Items API (type=RM)에서 조회 +// MOCK_EXPENSES 제거됨 - getExpenseItemOptions() 사용 \ No newline at end of file diff --git a/src/components/business/construction/item-management/ItemDetailClient.tsx b/src/components/business/construction/item-management/ItemDetailClient.tsx index d4717566..ee84cb25 100644 --- a/src/components/business/construction/item-management/ItemDetailClient.tsx +++ b/src/components/business/construction/item-management/ItemDetailClient.tsx @@ -183,8 +183,10 @@ export default function ItemDetailClient({ setIsSaving(true); try { + console.log('📤 [handleSave] formData:', formData); if (mode === 'new') { const result = await createItem(formData); + console.log('📥 [handleSave] createItem result:', result); if (result.success && result.data) { toast.success('품목이 등록되었습니다.'); router.push(`/ko/construction/order/base-info/items/${result.data.id}`); @@ -193,6 +195,7 @@ export default function ItemDetailClient({ } } else if (mode === 'edit' && itemId) { const result = await updateItem(itemId, formData); + console.log('📥 [handleSave] updateItem result:', result); if (result.success) { toast.success('품목이 수정되었습니다.'); setMode('view'); @@ -205,7 +208,8 @@ export default function ItemDetailClient({ toast.error(result.error || '품목 수정에 실패했습니다.'); } } - } catch { + } catch (error) { + console.error('❌ [handleSave] 오류:', error); toast.error('저장 중 오류가 발생했습니다.'); } finally { setIsSaving(false); @@ -362,7 +366,9 @@ export default function ItemDetailClient({ />
- +