fix: 견적관리 공과 품목 API 파라미터 및 MES 기능 개선
- 공과 상세 셀렉트 박스 API 파라미터 수정 (type → item_type) - VendorManagement 목록 컴포넌트 개선 - 작업지시/작업실적 타입 및 UI 개선 - 검사관리 actions 수정
This commit is contained in:
BIN
.serena/.DS_Store
vendored
Normal file
BIN
.serena/.DS_Store
vendored
Normal file
Binary file not shown.
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
81
.serena/memories/quote-registration-formfield-fix.md
Normal file
81
.serena/memories/quote-registration-formfield-fix.md
Normal file
@@ -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={value} onChange={...} /> // ← 자체 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. 확인 필요: 수량 변경 시 견적 산출 결과가 실시간으로 업데이트되는지 테스트
|
||||
70
.serena/memories/receivables-dynamic-months-fix.md
Normal file
70
.serena/memories/receivables-dynamic-months-fix.md
Normal file
@@ -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 개선 (사용자 확인 필요)
|
||||
84
.serena/project.yml
Normal file
84
.serena/project.yml
Normal file
@@ -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: []
|
||||
BIN
claudedocs/.DS_Store
vendored
BIN
claudedocs/.DS_Store
vendored
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
> **작성일**: 2025-12-04
|
||||
> **목적**: 거래처관리 페이지 API 연동 및 sam-design 기준 UI 구현
|
||||
ㅇ> **최종 업데이트**: 2025-12-04 ✅ 구현 완료
|
||||
> **최종 업데이트**: 2025-12-04 ✅ 구현 완료
|
||||
|
||||
---
|
||||
|
||||
|
||||
158
deploy.sh
Executable file
158
deploy.sh
Executable file
@@ -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 "=========================================="
|
||||
BIN
next-build_0113.tar.gz
Normal file
BIN
next-build_0113.tar.gz
Normal file
Binary file not shown.
11
package-lock.json
generated
11
package-lock.json
generated
@@ -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",
|
||||
|
||||
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -302,7 +302,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) {
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
ㄷ <Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600" disabled={isSaving}>
|
||||
<Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600" disabled={isSaving}>
|
||||
수정
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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 = (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 구분 필터 */}
|
||||
<Select value={categoryFilter} onValueChange={(value) => handleFilterChange('category', value)}>
|
||||
<SelectTrigger className="w-[100px] h-8">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{VENDOR_CATEGORY_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 신용등급 필터 */}
|
||||
<Select value={creditRatingFilter} onValueChange={(value) => handleFilterChange('creditRating', value)}>
|
||||
<SelectTrigger className="w-[100px] h-8">
|
||||
<SelectValue placeholder="신용등급" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CREDIT_RATING_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 거래등급 필터 */}
|
||||
<Select value={transactionGradeFilter} onValueChange={(value) => handleFilterChange('transactionGrade', value)}>
|
||||
<SelectTrigger className="w-[100px] h-8">
|
||||
<SelectValue placeholder="거래등급" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TRANSACTION_GRADE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 악성채권 필터 */}
|
||||
<Select value={badDebtFilter} onValueChange={(value) => handleFilterChange('badDebt', value)}>
|
||||
<SelectTrigger className="w-[100px] h-8">
|
||||
<SelectValue placeholder="악성채권" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BAD_DEBT_STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 정렬 필터 */}
|
||||
<Select value={sortOption} onValueChange={(value) => handleFilterChange('sort', value)}>
|
||||
<SelectTrigger className="w-[140px] h-8">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 필터 초기화 버튼 */}
|
||||
<Button variant="outline" size="sm" onClick={handleFilterReset} className="h-8">
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedListTemplateV2
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, Loader2, List } from 'lucide-react';
|
||||
import { getExpenseItemOptions, type ExpenseItemOption } from './actions';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -29,7 +30,7 @@ import type {
|
||||
import { getEmptyEstimateDetailFormData, estimateDetailToFormData } from './types';
|
||||
import { ElectronicApprovalModal } from './modals/ElectronicApprovalModal';
|
||||
import { EstimateDocumentModal } from './modals/EstimateDocumentModal';
|
||||
import { MOCK_MATERIALS, MOCK_EXPENSES } from './utils';
|
||||
import { MOCK_MATERIALS } from './utils';
|
||||
import {
|
||||
EstimateInfoSection,
|
||||
EstimateSummarySection,
|
||||
@@ -75,6 +76,20 @@ export default function EstimateDetailForm({
|
||||
// 드래그 상태
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// 공과 품목 옵션 (Items API에서 조회)
|
||||
const [expenseOptions, setExpenseOptions] = useState<ExpenseItemOption[]>([]);
|
||||
|
||||
// 공과 품목 옵션 조회
|
||||
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({
|
||||
{/* 공과 상세 */}
|
||||
<ExpenseDetailSection
|
||||
expenseItems={formData.expenseItems}
|
||||
expenseOptions={expenseOptions.map((opt) => ({ value: opt.value, label: opt.label }))}
|
||||
isViewMode={isViewMode}
|
||||
onAddItems={handleAddExpenseItems}
|
||||
onRemoveSelected={handleRemoveSelectedExpenseItems}
|
||||
|
||||
@@ -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: '공과 품목 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
<SelectValue placeholder="공과 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MOCK_EXPENSES.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
{expenseOptions.length === 0 ? (
|
||||
<SelectItem value="_empty" disabled>
|
||||
등록된 공과 품목이 없습니다
|
||||
</SelectItem>
|
||||
))}
|
||||
) : (
|
||||
expenseOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
// 공과 품목은 Items API (type=RM)에서 조회
|
||||
// MOCK_EXPENSES 제거됨 - getExpenseItemOptions() 사용
|
||||
@@ -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({
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="itemType">품목유형</Label>
|
||||
<Label htmlFor="itemType">
|
||||
품목유형 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.itemType}
|
||||
onValueChange={(v) => handleFieldChange('itemType', v as ItemType)}
|
||||
|
||||
@@ -383,7 +383,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
|
||||
<List className="h-4 w-4 mr-2" />
|
||||
목록
|
||||
</Button>
|
||||
ㅇ <Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600">
|
||||
<Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600">
|
||||
수정
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -345,17 +345,47 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
</Button>
|
||||
)}
|
||||
{order.status === 'in_progress' && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleStatusChange('waiting')}
|
||||
disabled={isStatusUpdating}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{isStatusUpdating ? (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<Undo2 className="w-4 h-4 mr-1.5" />
|
||||
)}
|
||||
작업 취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleStatusChange('completed')}
|
||||
disabled={isStatusUpdating}
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
{isStatusUpdating ? (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-4 h-4 mr-1.5" />
|
||||
)}
|
||||
작업 완료
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{order.status === 'completed' && (
|
||||
<Button
|
||||
onClick={() => handleStatusChange('completed')}
|
||||
variant="outline"
|
||||
onClick={() => handleStatusChange('in_progress')}
|
||||
disabled={isStatusUpdating}
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
className="text-orange-600 hover:text-orange-700 border-orange-300 hover:bg-orange-50"
|
||||
>
|
||||
{isStatusUpdating ? (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-4 h-4 mr-1.5" />
|
||||
<Undo2 className="w-4 h-4 mr-1.5" />
|
||||
)}
|
||||
작업 완료
|
||||
되돌리기
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => setIsWorkLogOpen(true)}>
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { toast } from 'sonner';
|
||||
import { getWorkResults, getWorkResultStats } from './actions';
|
||||
import { PROCESS_TYPE_LABELS } from '../WorkOrders/types';
|
||||
import type { WorkResult, WorkResultStats } from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
@@ -184,6 +183,12 @@ export function WorkResultList() {
|
||||
// TODO: 상세 보기 기능 구현
|
||||
}, []);
|
||||
|
||||
// 작업일 포맷팅 (날짜만 표시)
|
||||
const formatWorkDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split(' ')[0]; // "2025-01-14 10:30:00" → "2025-01-14"
|
||||
};
|
||||
|
||||
// 테이블 행 렌더링
|
||||
const renderTableRow = (result: WorkResult, index: number, globalIndex: number) => {
|
||||
const isSelected = selectedItems.has(result.id);
|
||||
@@ -202,11 +207,11 @@ export function WorkResultList() {
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
|
||||
<TableCell className="font-medium">{result.lotNo}</TableCell>
|
||||
<TableCell>{result.workDate}</TableCell>
|
||||
<TableCell>{formatWorkDate(result.workDate)}</TableCell>
|
||||
<TableCell>{result.workOrderNo}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{PROCESS_TYPE_LABELS[result.processType]}
|
||||
{result.processName || '-'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate">{result.productName}</TableCell>
|
||||
@@ -237,7 +242,7 @@ export function WorkResultList() {
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{result.worker}</TableCell>
|
||||
<TableCell>{result.workerId ? `#${result.workerId}` : '-'}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@@ -265,15 +270,15 @@ export function WorkResultList() {
|
||||
title={result.productName}
|
||||
statusBadge={
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{PROCESS_TYPE_LABELS[result.processType]}
|
||||
{result.processName || '-'}
|
||||
</Badge>
|
||||
}
|
||||
infoGrid={
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<InfoField label="작업일" value={result.workDate} />
|
||||
<InfoField label="작업일" value={formatWorkDate(result.workDate)} />
|
||||
<InfoField label="작업지시번호" value={result.workOrderNo} />
|
||||
<InfoField label="규격" value={result.specification} />
|
||||
<InfoField label="작업자" value={result.worker} />
|
||||
<InfoField label="작업자" value={result.workerId ? `#${result.workerId}` : '-'} />
|
||||
<InfoField label="생산수량" value={`${result.productionQty}개`} />
|
||||
<InfoField label="양품수량" value={`${result.goodQty}개`} />
|
||||
<InfoField
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
/**
|
||||
* 작업실적 조회 타입 정의
|
||||
*
|
||||
* WorkOrderItem.options.result 기반 작업실적 조회
|
||||
*/
|
||||
|
||||
import { ProcessType } from '../WorkOrders/types';
|
||||
|
||||
// 작업실적 데이터
|
||||
// 작업실적 데이터 (Frontend)
|
||||
export interface WorkResult {
|
||||
id: string;
|
||||
lotNo: string; // 로트번호
|
||||
workDate: string; // 작업일
|
||||
workDate: string; // 작업 완료일
|
||||
workOrderNo: string; // 작업지시번호
|
||||
processType: ProcessType; // 공정
|
||||
projectName: string; // 프로젝트명
|
||||
processName: string; // 공정명
|
||||
processCode: string; // 공정코드
|
||||
productName: string; // 품목명
|
||||
specification: string; // 규격
|
||||
productionQty: number; // 생산수량
|
||||
quantity: number; // 지시수량
|
||||
productionQty: number; // 생산수량 (양품+불량)
|
||||
goodQty: number; // 양품수량
|
||||
defectQty: number; // 불량수량
|
||||
defectRate: number; // 불량률 (%)
|
||||
inspection: boolean; // 검사
|
||||
packaging: boolean; // 포장
|
||||
worker: string; // 작업자
|
||||
inspection: boolean; // 검사 완료
|
||||
packaging: boolean; // 포장 완료
|
||||
workerId: number | null; // 작업자 ID
|
||||
memo: string | null; // 메모
|
||||
}
|
||||
|
||||
// 통계 데이터
|
||||
@@ -32,30 +36,49 @@ export interface WorkResultStats {
|
||||
|
||||
// ===== API 타입 정의 =====
|
||||
|
||||
// API 응답 - 작업실적
|
||||
export interface WorkResultApi {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
work_order_id: number;
|
||||
work_order_item_id: number | null;
|
||||
lot_no: string;
|
||||
work_date: string;
|
||||
process_type: 'screen' | 'slat' | 'bending';
|
||||
product_name: string;
|
||||
specification: string | null;
|
||||
production_qty: number;
|
||||
// 작업 결과 데이터 (options.result JSON)
|
||||
export interface WorkResultData {
|
||||
completed_at: string;
|
||||
good_qty: number;
|
||||
defect_qty: number;
|
||||
defect_rate: number;
|
||||
lot_no: string | null;
|
||||
is_inspected: boolean;
|
||||
is_packaged: boolean;
|
||||
worker_id: number | null;
|
||||
memo: string | null;
|
||||
}
|
||||
|
||||
// API 응답 - 작업실적 (WorkOrderItem 기반)
|
||||
export interface WorkResultApi {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
work_order_id: number;
|
||||
item_id: number | null;
|
||||
item_name: string;
|
||||
specification: string | null;
|
||||
quantity: string; // decimal
|
||||
unit: string | null;
|
||||
sort_order: number;
|
||||
status: 'waiting' | 'in_progress' | 'completed';
|
||||
options: {
|
||||
result?: WorkResultData;
|
||||
} | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
// Relations
|
||||
work_order?: { id: number; work_order_no: string };
|
||||
worker?: { id: number; name: string };
|
||||
work_order?: {
|
||||
id: number;
|
||||
work_order_no: string;
|
||||
project_name: string | null;
|
||||
process_id: number | null;
|
||||
completed_at: string | null;
|
||||
process?: {
|
||||
id: number;
|
||||
process_name: string;
|
||||
process_code: string;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
// API 페이징 응답
|
||||
@@ -79,40 +102,42 @@ export interface WorkResultStatsApi {
|
||||
|
||||
// API → Frontend 변환
|
||||
export function transformApiToFrontend(api: WorkResultApi): WorkResult {
|
||||
const result = api.options?.result;
|
||||
const goodQty = result?.good_qty ?? 0;
|
||||
const defectQty = result?.defect_qty ?? 0;
|
||||
|
||||
return {
|
||||
id: String(api.id),
|
||||
lotNo: api.lot_no,
|
||||
workDate: api.work_date,
|
||||
lotNo: result?.lot_no || '-',
|
||||
workDate: result?.completed_at || api.updated_at,
|
||||
workOrderNo: api.work_order?.work_order_no || '-',
|
||||
processType: api.process_type,
|
||||
productName: api.product_name,
|
||||
projectName: api.work_order?.project_name || '-',
|
||||
processName: api.work_order?.process?.process_name || '-',
|
||||
processCode: api.work_order?.process?.process_code || '-',
|
||||
productName: api.item_name,
|
||||
specification: api.specification || '-',
|
||||
productionQty: api.production_qty,
|
||||
goodQty: api.good_qty,
|
||||
defectQty: api.defect_qty,
|
||||
defectRate: api.defect_rate,
|
||||
inspection: api.is_inspected,
|
||||
packaging: api.is_packaged,
|
||||
worker: api.worker?.name || '-',
|
||||
quantity: parseFloat(api.quantity) || 0,
|
||||
productionQty: goodQty + defectQty,
|
||||
goodQty: goodQty,
|
||||
defectQty: defectQty,
|
||||
defectRate: result?.defect_rate ?? 0,
|
||||
inspection: result?.is_inspected ?? false,
|
||||
packaging: result?.is_packaged ?? false,
|
||||
workerId: result?.worker_id ?? null,
|
||||
memo: result?.memo ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// Frontend → API 변환 (등록/수정용)
|
||||
export function transformFrontendToApi(data: Partial<WorkResult> & { workOrderId?: number; workerId?: number }): Record<string, unknown> {
|
||||
// Frontend → API 변환 (수정용)
|
||||
export function transformFrontendToApi(data: Partial<WorkResult>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (data.workOrderId !== undefined) result.work_order_id = data.workOrderId;
|
||||
if (data.lotNo !== undefined) result.lot_no = data.lotNo;
|
||||
if (data.workDate !== undefined) result.work_date = data.workDate;
|
||||
if (data.processType !== undefined) result.process_type = data.processType;
|
||||
if (data.productName !== undefined) result.product_name = data.productName;
|
||||
if (data.specification !== undefined) result.specification = data.specification;
|
||||
if (data.productionQty !== undefined) result.production_qty = data.productionQty;
|
||||
if (data.goodQty !== undefined) result.good_qty = data.goodQty;
|
||||
if (data.defectQty !== undefined) result.defect_qty = data.defectQty;
|
||||
if (data.inspection !== undefined) result.is_inspected = data.inspection;
|
||||
if (data.packaging !== undefined) result.is_packaged = data.packaging;
|
||||
if (data.workerId !== undefined) result.worker_id = data.workerId;
|
||||
if (data.memo !== undefined) result.memo = data.memo;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ interface InspectionApiItem {
|
||||
}
|
||||
|
||||
interface InspectionApiPaginatedResponse {
|
||||
data: InspectionApiItem[];
|
||||
items: InspectionApiItem[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
@@ -247,7 +247,7 @@ export async function getInspections(params?: {
|
||||
}
|
||||
|
||||
const paginatedData: InspectionApiPaginatedResponse = result.data || {
|
||||
data: [],
|
||||
items: [],
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 20,
|
||||
@@ -256,7 +256,7 @@ export async function getInspections(params?: {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: paginatedData.data.map(transformApiToFrontend),
|
||||
data: paginatedData.items.map(transformApiToFrontend),
|
||||
pagination: {
|
||||
currentPage: paginatedData.current_page,
|
||||
lastPage: paginatedData.last_page,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user