817 lines
26 KiB
Markdown
817 lines
26 KiB
Markdown
|
|
# 입찰관리(Bidding) API 구현 계획
|
||
|
|
|
||
|
|
> **작성일**: 2026-01-19
|
||
|
|
> **목적**: 견적 → 입찰 전환 기능 구현 및 테스트용 더미데이터 생성
|
||
|
|
> **기준 문서**: React 목업 타입 (`react/src/components/business/construction/bidding/types.ts`)
|
||
|
|
> **상태**: ✅ 완료 (Serena ID: bidding-api-state)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📍 현재 진행 상태
|
||
|
|
|
||
|
|
| 항목 | 내용 |
|
||
|
|
|------|------|
|
||
|
|
| **마지막 완료 작업** | Phase 4.3 - Pint 코드 포맷팅 및 Swagger 재생성 |
|
||
|
|
| **다음 작업** | 사용자 수동 실행 (마이그레이션, 시더) |
|
||
|
|
| **진행률** | 12/12 (100%) |
|
||
|
|
| **마지막 업데이트** | 2026-01-19 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 개요
|
||
|
|
|
||
|
|
### 1.1 배경
|
||
|
|
|
||
|
|
**업무 흐름:**
|
||
|
|
```
|
||
|
|
현장설명회 → 견적관리 → [견적완료] → 입찰관리 → 계약관리 → 기성/정산
|
||
|
|
↑
|
||
|
|
전환 기능 필요
|
||
|
|
```
|
||
|
|
|
||
|
|
현재 React 프론트엔드의 입찰관리(`/construction/project/bidding`)는 **목업 데이터**를 사용 중입니다.
|
||
|
|
견적(Quote) API는 이미 구현되어 있으므로, 입찰(Bidding) API를 새로 구현하고 견적 → 입찰 전환 기능을 추가해야 합니다.
|
||
|
|
|
||
|
|
**현재 상태:**
|
||
|
|
| 구분 | 견적(Estimate/Quote) | 입찰(Bidding) |
|
||
|
|
|------|---------------------|---------------|
|
||
|
|
| API Model | ✅ `Estimate.php` | ❌ 없음 |
|
||
|
|
| API Migration | ✅ `estimates` 테이블 | ❌ 없음 |
|
||
|
|
| API Endpoint | ✅ `/api/v1/quotes` | ❌ 없음 |
|
||
|
|
| React | ✅ API 연동 완료 | ❌ 목업 상태 |
|
||
|
|
|
||
|
|
### 1.2 기준 원칙
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ 🎯 핵심 원칙 │
|
||
|
|
├─────────────────────────────────────────────────────────────────┤
|
||
|
|
│ 1. SAM API Rules 엄격 준수 (Service-First, FormRequest) │
|
||
|
|
│ 2. Multi-tenancy 필수 (BelongsToTenant) │
|
||
|
|
│ 3. React 목업 타입과 100% 호환 │
|
||
|
|
│ 4. 견적 데이터 참조 (복사가 아닌 FK 연결) │
|
||
|
|
└─────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 변경 승인 정책
|
||
|
|
|
||
|
|
| 분류 | 예시 | 승인 |
|
||
|
|
|------|------|------|
|
||
|
|
| ✅ 즉시 가능 | 새 테이블 생성, 새 API 추가, Seeder 작성 | 불필요 |
|
||
|
|
| ⚠️ 컨펌 필요 | 기존 quotes 테이블 수정, 비즈니스 로직 변경 | **필수** |
|
||
|
|
| 🔴 금지 | 기존 API 삭제, 파괴적 변경 | 별도 협의 |
|
||
|
|
|
||
|
|
### 1.4 준수 규칙
|
||
|
|
|
||
|
|
- `api/CLAUDE.md` - SAM API Development Rules
|
||
|
|
- `docs/standards/quality-checklist.md` - 품질 체크리스트
|
||
|
|
- `docs/guides/swagger-guide.md` - Swagger 문서화
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 대상 범위
|
||
|
|
|
||
|
|
### 2.1 Phase 1: Database & Model (Day 1)
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 1.1 | `biddings` 테이블 마이그레이션 생성 | ✅ | `2026_01_19_100000_create_biddings_table.php` |
|
||
|
|
| 1.2 | `Bidding` Model 생성 | ✅ | BelongsToTenant, SoftDeletes |
|
||
|
|
| 1.3 | 더미데이터 Seeder 생성 | ✅ | 10건 테스트 데이터 |
|
||
|
|
|
||
|
|
### 2.2 Phase 2: API Implementation (Day 2)
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 2.1 | BiddingService 생성 | ✅ | CRUD + 통계 |
|
||
|
|
| 2.2 | BiddingController 생성 | ✅ | |
|
||
|
|
| 2.3 | FormRequest 생성 | ✅ | Filter, Update, Status, BulkDelete |
|
||
|
|
| 2.4 | Routes 등록 | ✅ | `/api/v1/biddings` |
|
||
|
|
|
||
|
|
### 2.3 Phase 3: 견적 → 입찰 전환 (Day 2-3)
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 3.1 | QuoteService에 `convertToBidding()` 추가 | ✅ | 기존 코드에 메서드 추가 |
|
||
|
|
| 3.2 | 전환 API 엔드포인트 추가 | ✅ | `POST /quotes/{id}/convert-to-bidding` |
|
||
|
|
|
||
|
|
### 2.4 Phase 4: Swagger & 검증 (Day 3)
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 4.1 | Swagger 문서 작성 | ✅ | `BiddingApi.php` |
|
||
|
|
| 4.2 | i18n 메시지 추가 | ✅ | message.php, error.php |
|
||
|
|
| 4.3 | Pint 코드 포맷팅 | ✅ | 9 style issues fixed |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 작업 절차
|
||
|
|
|
||
|
|
### 3.1 단계별 절차
|
||
|
|
|
||
|
|
```
|
||
|
|
Step 1: Database Schema
|
||
|
|
├── biddings 테이블 마이그레이션 작성
|
||
|
|
├── 마이그레이션 실행
|
||
|
|
└── Seeder로 더미데이터 생성
|
||
|
|
|
||
|
|
Step 2: Model & Service
|
||
|
|
├── Bidding Model 생성 (BelongsToTenant, SoftDeletes)
|
||
|
|
├── BiddingService 생성 (CRUD, stats, filter)
|
||
|
|
└── BiddingController 생성
|
||
|
|
|
||
|
|
Step 3: API Routes
|
||
|
|
├── routes/api.php에 biddings 라우트 추가
|
||
|
|
├── FormRequest 클래스 생성
|
||
|
|
└── API 테스트
|
||
|
|
|
||
|
|
Step 4: 견적 → 입찰 전환
|
||
|
|
├── QuoteService에 convertToBidding() 추가
|
||
|
|
├── 전환 API 엔드포인트 추가
|
||
|
|
└── 전환 테스트
|
||
|
|
|
||
|
|
Step 5: Documentation
|
||
|
|
├── Swagger 문서 작성
|
||
|
|
├── API 문서 검증
|
||
|
|
└── Pint 실행
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 데이터베이스 스키마
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- biddings 테이블
|
||
|
|
CREATE TABLE biddings (
|
||
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
|
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID',
|
||
|
|
|
||
|
|
-- 기본 정보
|
||
|
|
bidding_code VARCHAR(50) NOT NULL COMMENT '입찰번호',
|
||
|
|
quote_id BIGINT UNSIGNED NULL COMMENT '연결된 견적 ID (quotes.id)',
|
||
|
|
|
||
|
|
-- 거래처/현장
|
||
|
|
client_id BIGINT UNSIGNED NULL COMMENT '거래처 ID',
|
||
|
|
client_name VARCHAR(100) NULL COMMENT '거래처명 (스냅샷)',
|
||
|
|
project_name VARCHAR(200) NULL COMMENT '현장명',
|
||
|
|
|
||
|
|
-- 입찰 정보
|
||
|
|
bidding_date DATE NULL COMMENT '입찰일',
|
||
|
|
bid_date DATE NULL COMMENT '입찰일 (레거시 호환)',
|
||
|
|
submission_date DATE NULL COMMENT '투찰일',
|
||
|
|
confirm_date DATE NULL COMMENT '확정일',
|
||
|
|
total_count INT DEFAULT 0 COMMENT '총 개소',
|
||
|
|
bidding_amount DECIMAL(15,2) DEFAULT 0 COMMENT '입찰금액',
|
||
|
|
|
||
|
|
-- 상태
|
||
|
|
status VARCHAR(20) DEFAULT 'waiting' COMMENT '상태 (waiting/submitted/failed/invalid/awarded/hold)',
|
||
|
|
|
||
|
|
-- 입찰자
|
||
|
|
bidder_id BIGINT UNSIGNED NULL COMMENT '입찰자 ID',
|
||
|
|
bidder_name VARCHAR(50) NULL COMMENT '입찰자명 (스냅샷)',
|
||
|
|
|
||
|
|
-- 공사기간
|
||
|
|
construction_start_date DATE NULL COMMENT '공사 시작일',
|
||
|
|
construction_end_date DATE NULL COMMENT '공사 종료일',
|
||
|
|
vat_type VARCHAR(20) DEFAULT 'excluded' COMMENT '부가세 (included/excluded)',
|
||
|
|
|
||
|
|
-- 비고
|
||
|
|
remarks TEXT NULL COMMENT '비고',
|
||
|
|
|
||
|
|
-- 견적 데이터 스냅샷 (JSON)
|
||
|
|
expense_items JSON NULL COMMENT '공과 항목 스냅샷',
|
||
|
|
estimate_detail_items JSON NULL COMMENT '견적 상세 항목 스냅샷',
|
||
|
|
|
||
|
|
-- 감사
|
||
|
|
created_by BIGINT UNSIGNED NULL COMMENT '생성자',
|
||
|
|
updated_by BIGINT UNSIGNED NULL COMMENT '수정자',
|
||
|
|
deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자',
|
||
|
|
created_at TIMESTAMP NULL,
|
||
|
|
updated_at TIMESTAMP NULL,
|
||
|
|
deleted_at TIMESTAMP NULL,
|
||
|
|
|
||
|
|
-- 인덱스
|
||
|
|
INDEX idx_tenant_id (tenant_id),
|
||
|
|
INDEX idx_status (status),
|
||
|
|
INDEX idx_bidding_date (bidding_date),
|
||
|
|
INDEX idx_quote_id (quote_id),
|
||
|
|
UNIQUE INDEX idx_bidding_code (tenant_id, bidding_code)
|
||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 API 엔드포인트 설계
|
||
|
|
|
||
|
|
| Method | Path | 설명 |
|
||
|
|
|--------|------|------|
|
||
|
|
| GET | `/api/v1/biddings` | 목록 조회 (필터, 페이지네이션) |
|
||
|
|
| GET | `/api/v1/biddings/stats` | 통계 조회 |
|
||
|
|
| GET | `/api/v1/biddings/{id}` | 단건 조회 |
|
||
|
|
| PUT | `/api/v1/biddings/{id}` | 수정 |
|
||
|
|
| DELETE | `/api/v1/biddings/{id}` | 삭제 |
|
||
|
|
| DELETE | `/api/v1/biddings/bulk` | 일괄 삭제 |
|
||
|
|
| POST | `/api/v1/quotes/{id}/convert-to-bidding` | 견적 → 입찰 전환 |
|
||
|
|
|
||
|
|
**참고**: 입찰은 별도 등록 없음 (견적완료 시 자동 전환)
|
||
|
|
|
||
|
|
### 3.4 타입 매핑 (React → API)
|
||
|
|
|
||
|
|
| React (camelCase) | API (snake_case) | DB Column |
|
||
|
|
|-------------------|------------------|-----------|
|
||
|
|
| `id` | `id` | `id` |
|
||
|
|
| `biddingCode` | `bidding_code` | `bidding_code` |
|
||
|
|
| `partnerId` | `client_id` | `client_id` |
|
||
|
|
| `partnerName` | `client_name` | `client_name` |
|
||
|
|
| `projectName` | `project_name` | `project_name` |
|
||
|
|
| `biddingDate` | `bidding_date` | `bidding_date` |
|
||
|
|
| `totalCount` | `total_count` | `total_count` |
|
||
|
|
| `biddingAmount` | `bidding_amount` | `bidding_amount` |
|
||
|
|
| `bidDate` | `bid_date` | `bid_date` |
|
||
|
|
| `submissionDate` | `submission_date` | `submission_date` |
|
||
|
|
| `confirmDate` | `confirm_date` | `confirm_date` |
|
||
|
|
| `status` | `status` | `status` |
|
||
|
|
| `bidderId` | `bidder_id` | `bidder_id` |
|
||
|
|
| `bidderName` | `bidder_name` | `bidder_name` |
|
||
|
|
| `remarks` | `remarks` | `remarks` |
|
||
|
|
| `estimateId` | `quote_id` | `quote_id` |
|
||
|
|
| `estimateCode` | `quote_number` | (join) |
|
||
|
|
|
||
|
|
### 3.5 상태값 매핑
|
||
|
|
|
||
|
|
| 값 | 한글 | 설명 |
|
||
|
|
|----|------|------|
|
||
|
|
| `waiting` | 입찰대기 | 견적 전환 후 초기 상태 |
|
||
|
|
| `submitted` | 투찰 | 투찰서 제출 완료 |
|
||
|
|
| `failed` | 탈락 | 입찰 실패 |
|
||
|
|
| `invalid` | 유찰 | 입찰 무효 |
|
||
|
|
| `awarded` | 낙찰 | 입찰 성공 |
|
||
|
|
| `hold` | 보류 | 검토 대기 |
|
||
|
|
|
||
|
|
### 3.6 기존 quotes 테이블 스키마 (연결용)
|
||
|
|
|
||
|
|
> `biddings.quote_id` → `quotes.id` FK 연결
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- quotes 테이블 핵심 컬럼 (api/database/migrations/2025_12_04_164542_create_quotes_table.php)
|
||
|
|
quotes (
|
||
|
|
id BIGINT PRIMARY KEY,
|
||
|
|
tenant_id BIGINT NOT NULL,
|
||
|
|
quote_type ENUM('manufacturing', 'construction'), -- 'construction' 필터
|
||
|
|
quote_number VARCHAR(50), -- 견적번호 (예: KD-SC-251204-01)
|
||
|
|
registration_date DATE,
|
||
|
|
client_id BIGINT, -- 거래처 ID
|
||
|
|
client_name VARCHAR(100), -- 거래처명
|
||
|
|
site_name VARCHAR(200), -- 현장명
|
||
|
|
total_amount DECIMAL(15,2), -- 최종 금액
|
||
|
|
status ENUM('pending','draft','sent','approved','rejected','finalized','converted'),
|
||
|
|
site_briefing_id BIGINT, -- 현장설명회 연결
|
||
|
|
options JSON, -- { summary_items, expense_items, detail_items, price_adjustment_data }
|
||
|
|
...
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Quote 상태 상수** (api/app/Models/Quote/Quote.php):
|
||
|
|
- `pending` → 견적대기 (현장설명회에서 자동생성)
|
||
|
|
- `finalized` → 확정 (입찰 전환 가능)
|
||
|
|
- `converted` → 전환완료
|
||
|
|
|
||
|
|
### 3.7 API 응답 형식 (JSON)
|
||
|
|
|
||
|
|
#### 목록 조회 응답 (GET /biddings)
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"message": "message.fetched",
|
||
|
|
"data": {
|
||
|
|
"data": [
|
||
|
|
{
|
||
|
|
"id": 1,
|
||
|
|
"bidding_code": "BID-2025-001",
|
||
|
|
"client_id": 1,
|
||
|
|
"client_name": "이사대표",
|
||
|
|
"project_name": "광장 아파트",
|
||
|
|
"bidding_date": "2025-01-25",
|
||
|
|
"total_count": 15,
|
||
|
|
"bidding_amount": 71000000,
|
||
|
|
"bid_date": "2025-01-20",
|
||
|
|
"submission_date": "2025-01-22",
|
||
|
|
"confirm_date": "2025-01-25",
|
||
|
|
"status": "awarded",
|
||
|
|
"bidder_id": 1,
|
||
|
|
"bidder_name": "홍길동",
|
||
|
|
"remarks": "",
|
||
|
|
"quote_id": 1,
|
||
|
|
"quote_number": "EST-2025-001",
|
||
|
|
"created_at": "2025-01-01T00:00:00.000000Z"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"current_page": 1,
|
||
|
|
"per_page": 20,
|
||
|
|
"total": 10,
|
||
|
|
"last_page": 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 통계 응답 (GET /biddings/stats)
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"message": "message.fetched",
|
||
|
|
"data": {
|
||
|
|
"total": 10,
|
||
|
|
"waiting": 3,
|
||
|
|
"awarded": 3
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 단건 조회 응답 (GET /biddings/{id})
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"message": "message.fetched",
|
||
|
|
"data": {
|
||
|
|
"id": 1,
|
||
|
|
"bidding_code": "BID-2025-001",
|
||
|
|
"client_id": 1,
|
||
|
|
"client_name": "이사대표",
|
||
|
|
"project_name": "광장 아파트",
|
||
|
|
"bidding_date": "2025-01-25",
|
||
|
|
"total_count": 15,
|
||
|
|
"bidding_amount": 71000000,
|
||
|
|
"status": "awarded",
|
||
|
|
"construction_start_date": "2025-02-01",
|
||
|
|
"construction_end_date": "2025-04-30",
|
||
|
|
"vat_type": "excluded",
|
||
|
|
"expense_items": [
|
||
|
|
{ "id": "1", "name": "설계비", "amount": 5000000 },
|
||
|
|
{ "id": "2", "name": "운반비", "amount": 3000000 }
|
||
|
|
],
|
||
|
|
"estimate_detail_items": [
|
||
|
|
{ "id": "1", "no": 1, "name": "방화문", "material": "SUS304", "width": 1000, "height": 2100, "quantity": 10, ... }
|
||
|
|
],
|
||
|
|
"quote": {
|
||
|
|
"id": 1,
|
||
|
|
"quote_number": "EST-2025-001"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.8 convertToBidding() 상세 로직
|
||
|
|
|
||
|
|
```php
|
||
|
|
/**
|
||
|
|
* 견적 → 입찰 전환
|
||
|
|
*
|
||
|
|
* @param int $quoteId 견적 ID
|
||
|
|
* @return Bidding 생성된 입찰
|
||
|
|
*/
|
||
|
|
public function convertToBidding(int $quoteId): Bidding
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
$userId = $this->apiUserId();
|
||
|
|
|
||
|
|
// 1. 견적 조회 (quote_type=construction, status=finalized)
|
||
|
|
$quote = Quote::where('tenant_id', $tenantId)
|
||
|
|
->where('id', $quoteId)
|
||
|
|
->where('quote_type', 'construction')
|
||
|
|
->where('status', 'finalized')
|
||
|
|
->firstOrFail();
|
||
|
|
|
||
|
|
// 2. 이미 입찰이 존재하는지 확인
|
||
|
|
$existingBidding = Bidding::where('quote_id', $quoteId)->first();
|
||
|
|
if ($existingBidding) {
|
||
|
|
throw new BadRequestHttpException(__('error.bidding_already_exists'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 입찰 데이터 생성
|
||
|
|
$bidding = Bidding::create([
|
||
|
|
'tenant_id' => $tenantId,
|
||
|
|
'bidding_code' => $this->generateBiddingCode($tenantId),
|
||
|
|
'quote_id' => $quote->id,
|
||
|
|
|
||
|
|
// 거래처/현장 정보 복사
|
||
|
|
'client_id' => $quote->client_id,
|
||
|
|
'client_name' => $quote->client_name,
|
||
|
|
'project_name' => $quote->site_name,
|
||
|
|
|
||
|
|
// 금액 정보
|
||
|
|
'bidding_amount' => $quote->total_amount,
|
||
|
|
'total_count' => $quote->items->count(),
|
||
|
|
|
||
|
|
// 날짜
|
||
|
|
'bidding_date' => now()->toDateString(),
|
||
|
|
|
||
|
|
// 상태
|
||
|
|
'status' => 'waiting',
|
||
|
|
|
||
|
|
// 현장설명회에서 공사기간 가져오기
|
||
|
|
'construction_start_date' => $quote->siteBriefing?->construction_start_date,
|
||
|
|
'construction_end_date' => $quote->siteBriefing?->construction_end_date,
|
||
|
|
'vat_type' => $quote->siteBriefing?->vat_type ?? 'excluded',
|
||
|
|
|
||
|
|
// 견적 옵션 데이터 스냅샷
|
||
|
|
'expense_items' => $quote->options['expense_items'] ?? [],
|
||
|
|
'estimate_detail_items' => $quote->options['detail_items'] ?? [],
|
||
|
|
|
||
|
|
'created_by' => $userId,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 4. 견적 상태 업데이트 (선택적)
|
||
|
|
// $quote->update(['status' => 'converted']);
|
||
|
|
|
||
|
|
return $bidding;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 입찰번호 자동 생성 (BID-YYYY-NNN)
|
||
|
|
*/
|
||
|
|
private function generateBiddingCode(int $tenantId): string
|
||
|
|
{
|
||
|
|
$year = now()->format('Y');
|
||
|
|
$prefix = "BID-{$year}-";
|
||
|
|
|
||
|
|
$lastBidding = Bidding::where('tenant_id', $tenantId)
|
||
|
|
->where('bidding_code', 'like', "{$prefix}%")
|
||
|
|
->orderBy('id', 'desc')
|
||
|
|
->first();
|
||
|
|
|
||
|
|
$sequence = 1;
|
||
|
|
if ($lastBidding) {
|
||
|
|
$lastNum = (int) substr($lastBidding->bidding_code, -3);
|
||
|
|
$sequence = $lastNum + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $prefix . str_pad($sequence, 3, '0', STR_PAD_LEFT);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.9 Service/Controller 패턴 (SAM 표준)
|
||
|
|
|
||
|
|
**Controller 패턴** (api/app/Http/Controllers):
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
namespace App\Http\Controllers\Api\v1;
|
||
|
|
|
||
|
|
use App\Helpers\ApiResponse;
|
||
|
|
use App\Http\Requests\Bidding\BiddingFilterRequest;
|
||
|
|
use App\Http\Requests\Bidding\BiddingUpdateRequest;
|
||
|
|
use App\Services\Bidding\BiddingService;
|
||
|
|
|
||
|
|
class BiddingController extends Controller
|
||
|
|
{
|
||
|
|
public function __construct(private BiddingService $service) {}
|
||
|
|
|
||
|
|
public function index(BiddingFilterRequest $request)
|
||
|
|
{
|
||
|
|
return ApiResponse::handle(fn () => $this->service->index($request->validated()));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function show(int $id)
|
||
|
|
{
|
||
|
|
return ApiResponse::handle(fn () => $this->service->show($id));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function update(BiddingUpdateRequest $request, int $id)
|
||
|
|
{
|
||
|
|
return ApiResponse::handle(fn () => $this->service->update($id, $request->validated()));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function destroy(int $id)
|
||
|
|
{
|
||
|
|
return ApiResponse::handle(fn () => $this->service->destroy($id));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function stats()
|
||
|
|
{
|
||
|
|
return ApiResponse::handle(fn () => $this->service->stats());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Service 패턴** (api/app/Services):
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
namespace App\Services\Bidding;
|
||
|
|
|
||
|
|
use App\Models\Bidding\Bidding;
|
||
|
|
use App\Services\Service;
|
||
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||
|
|
|
||
|
|
class BiddingService extends Service
|
||
|
|
{
|
||
|
|
public function index(array $params): LengthAwarePaginator
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId(); // 필수
|
||
|
|
$query = Bidding::where('tenant_id', $tenantId);
|
||
|
|
// ... 필터, 정렬, 페이지네이션
|
||
|
|
return $query->paginate($params['size'] ?? 20);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function show(int $id): Bidding
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
return Bidding::where('tenant_id', $tenantId)
|
||
|
|
->with(['quote'])
|
||
|
|
->findOrFail($id);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function stats(): array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
return [
|
||
|
|
'total' => Bidding::where('tenant_id', $tenantId)->count(),
|
||
|
|
'waiting' => Bidding::where('tenant_id', $tenantId)->where('status', 'waiting')->count(),
|
||
|
|
'awarded' => Bidding::where('tenant_id', $tenantId)->where('status', 'awarded')->count(),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.10 더미데이터 (Seeder용 10건)
|
||
|
|
|
||
|
|
> React 목업 기준 (`react/src/components/business/construction/bidding/actions.ts`)
|
||
|
|
|
||
|
|
```php
|
||
|
|
// api/database/seeders/BiddingSeeder.php
|
||
|
|
$biddings = [
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-001',
|
||
|
|
'client_name' => '이사대표',
|
||
|
|
'project_name' => '광장 아파트',
|
||
|
|
'bidding_date' => '2025-01-25',
|
||
|
|
'total_count' => 15,
|
||
|
|
'bidding_amount' => 71000000,
|
||
|
|
'bid_date' => '2025-01-20',
|
||
|
|
'submission_date' => '2025-01-22',
|
||
|
|
'confirm_date' => '2025-01-25',
|
||
|
|
'status' => 'awarded',
|
||
|
|
'bidder_name' => '홍길동',
|
||
|
|
'remarks' => '',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-002',
|
||
|
|
'client_name' => '야사건설',
|
||
|
|
'project_name' => '대림아파트',
|
||
|
|
'bidding_date' => '2025-01-20',
|
||
|
|
'total_count' => 22,
|
||
|
|
'bidding_amount' => 100000000,
|
||
|
|
'bid_date' => '2025-01-18',
|
||
|
|
'submission_date' => null,
|
||
|
|
'confirm_date' => null,
|
||
|
|
'status' => 'waiting',
|
||
|
|
'bidder_name' => '김철수',
|
||
|
|
'remarks' => '',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-003',
|
||
|
|
'client_name' => '여의건설',
|
||
|
|
'project_name' => '현장아파트',
|
||
|
|
'bidding_date' => '2025-01-18',
|
||
|
|
'total_count' => 18,
|
||
|
|
'bidding_amount' => 85000000,
|
||
|
|
'bid_date' => '2025-01-15',
|
||
|
|
'submission_date' => '2025-01-16',
|
||
|
|
'confirm_date' => '2025-01-18',
|
||
|
|
'status' => 'awarded',
|
||
|
|
'bidder_name' => '홍길동',
|
||
|
|
'remarks' => '',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-004',
|
||
|
|
'client_name' => '이사대표',
|
||
|
|
'project_name' => '송파타워',
|
||
|
|
'bidding_date' => '2025-01-15',
|
||
|
|
'total_count' => 30,
|
||
|
|
'bidding_amount' => 120000000,
|
||
|
|
'bid_date' => '2025-01-12',
|
||
|
|
'submission_date' => '2025-01-13',
|
||
|
|
'confirm_date' => '2025-01-15',
|
||
|
|
'status' => 'failed',
|
||
|
|
'bidder_name' => '이영희',
|
||
|
|
'remarks' => '가격 경쟁력 부족',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-005',
|
||
|
|
'client_name' => '야사건설',
|
||
|
|
'project_name' => '강남센터',
|
||
|
|
'bidding_date' => '2025-01-12',
|
||
|
|
'total_count' => 25,
|
||
|
|
'bidding_amount' => 95000000,
|
||
|
|
'bid_date' => '2025-01-10',
|
||
|
|
'submission_date' => '2025-01-11',
|
||
|
|
'confirm_date' => null,
|
||
|
|
'status' => 'submitted',
|
||
|
|
'bidder_name' => '홍길동',
|
||
|
|
'remarks' => '',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-006',
|
||
|
|
'client_name' => '여의건설',
|
||
|
|
'project_name' => '목동센터',
|
||
|
|
'bidding_date' => '2025-01-10',
|
||
|
|
'total_count' => 12,
|
||
|
|
'bidding_amount' => 78000000,
|
||
|
|
'bid_date' => '2025-01-08',
|
||
|
|
'submission_date' => '2025-01-09',
|
||
|
|
'confirm_date' => '2025-01-10',
|
||
|
|
'status' => 'invalid',
|
||
|
|
'bidder_name' => '김철수',
|
||
|
|
'remarks' => '입찰 조건 미충족',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-007',
|
||
|
|
'client_name' => '이사대표',
|
||
|
|
'project_name' => '서초타워',
|
||
|
|
'bidding_date' => '2025-01-08',
|
||
|
|
'total_count' => 35,
|
||
|
|
'bidding_amount' => 150000000,
|
||
|
|
'bid_date' => '2025-01-05',
|
||
|
|
'submission_date' => null,
|
||
|
|
'confirm_date' => null,
|
||
|
|
'status' => 'waiting',
|
||
|
|
'bidder_name' => '이영희',
|
||
|
|
'remarks' => '',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-008',
|
||
|
|
'client_name' => '야사건설',
|
||
|
|
'project_name' => '청담프로젝트',
|
||
|
|
'bidding_date' => '2025-01-05',
|
||
|
|
'total_count' => 40,
|
||
|
|
'bidding_amount' => 200000000,
|
||
|
|
'bid_date' => '2025-01-03',
|
||
|
|
'submission_date' => '2025-01-04',
|
||
|
|
'confirm_date' => '2025-01-05',
|
||
|
|
'status' => 'awarded',
|
||
|
|
'bidder_name' => '홍길동',
|
||
|
|
'remarks' => '',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-009',
|
||
|
|
'client_name' => '여의건설',
|
||
|
|
'project_name' => '잠실센터',
|
||
|
|
'bidding_date' => '2025-01-03',
|
||
|
|
'total_count' => 20,
|
||
|
|
'bidding_amount' => 88000000,
|
||
|
|
'bid_date' => '2025-01-01',
|
||
|
|
'submission_date' => null,
|
||
|
|
'confirm_date' => null,
|
||
|
|
'status' => 'hold',
|
||
|
|
'bidder_name' => '김철수',
|
||
|
|
'remarks' => '검토 대기 중',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'bidding_code' => 'BID-2025-010',
|
||
|
|
'client_name' => '이사대표',
|
||
|
|
'project_name' => '역삼빌딩',
|
||
|
|
'bidding_date' => '2025-01-01',
|
||
|
|
'total_count' => 10,
|
||
|
|
'bidding_amount' => 65000000,
|
||
|
|
'bid_date' => '2024-12-28',
|
||
|
|
'submission_date' => null,
|
||
|
|
'confirm_date' => null,
|
||
|
|
'status' => 'waiting',
|
||
|
|
'bidder_name' => '이영희',
|
||
|
|
'remarks' => '',
|
||
|
|
],
|
||
|
|
];
|
||
|
|
|
||
|
|
// 통계 요약:
|
||
|
|
// - total: 10건
|
||
|
|
// - waiting: 3건 (BID-002, 007, 010)
|
||
|
|
// - awarded: 3건 (BID-001, 003, 008)
|
||
|
|
// - submitted: 1건 (BID-005)
|
||
|
|
// - failed: 1건 (BID-004)
|
||
|
|
// - invalid: 1건 (BID-006)
|
||
|
|
// - hold: 1건 (BID-009)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 상세 작업 내용
|
||
|
|
|
||
|
|
> 각 Phase 진행 후 이 섹션에 상세 내용 추가
|
||
|
|
|
||
|
|
### 4.1 Phase 1: Database & Model
|
||
|
|
|
||
|
|
#### 1.1 마이그레이션 파일 생성
|
||
|
|
- **상태**: ⏳ 대기
|
||
|
|
- **파일**: `api/database/migrations/2026_01_19_XXXXXX_create_biddings_table.php`
|
||
|
|
|
||
|
|
#### 1.2 Model 생성
|
||
|
|
- **상태**: ⏳ 대기
|
||
|
|
- **파일**: `api/app/Models/Bidding/Bidding.php`
|
||
|
|
|
||
|
|
#### 1.3 Seeder 생성
|
||
|
|
- **상태**: ⏳ 대기
|
||
|
|
- **파일**: `api/database/seeders/BiddingSeeder.php`
|
||
|
|
- **데이터**: React 목업 기준 10건
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 컨펌 대기 목록
|
||
|
|
|
||
|
|
> API 내부 로직 변경 등 승인 필요 항목
|
||
|
|
|
||
|
|
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||
|
|
|---|------|----------|----------|------|
|
||
|
|
| 1 | QuoteService 수정 | `convertToBidding()` 메서드 추가 | api/Quote | ⏳ 대기 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 변경 이력
|
||
|
|
|
||
|
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||
|
|
|------|------|----------|------|------|
|
||
|
|
| 2026-01-19 | - | 문서 초안 작성 | - | - |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 참고 문서
|
||
|
|
|
||
|
|
- **SAM API Rules**: `api/CLAUDE.md`
|
||
|
|
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
|
||
|
|
- **Swagger 가이드**: `docs/guides/swagger-guide.md`
|
||
|
|
- **React 목업 타입**: `react/src/components/business/construction/bidding/types.ts`
|
||
|
|
- **React 목업 데이터**: `react/src/components/business/construction/bidding/actions.ts`
|
||
|
|
- **기존 견적 API**: `react/src/components/business/construction/estimates/actions.ts`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 세션 및 메모리 관리 정책 (Serena Optimized)
|
||
|
|
|
||
|
|
### 8.1 세션 시작 시 (Load Strategy)
|
||
|
|
```javascript
|
||
|
|
read_memory("bidding-api-state") // 1. 상태 파악
|
||
|
|
read_memory("bidding-api-snapshot") // 2. 사고 흐름 복구
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8.2 작업 중 관리 (Context Defense)
|
||
|
|
| 컨텍스트 잔량 | Action | 내용 |
|
||
|
|
|--------------|--------|------|
|
||
|
|
| **30% 이하** | 🛠 Snapshot | 현재까지 코드 변경점 저장 |
|
||
|
|
| **20% 이하** | 🧹 Context Purge | 활성 심볼 저장 |
|
||
|
|
| **10% 이하** | 🛑 Stop & Save | 최종 상태 저장 |
|
||
|
|
|
||
|
|
### 8.3 Serena 메모리 구조
|
||
|
|
- `bidding-api-state`: { phase, progress, next_step }
|
||
|
|
- `bidding-api-snapshot`: 현재까지의 코드 변경점 요약
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. 검증 결과
|
||
|
|
|
||
|
|
> 작업 완료 후 이 섹션에 검증 결과 추가
|
||
|
|
|
||
|
|
### 9.1 API 테스트 케이스
|
||
|
|
|
||
|
|
| 엔드포인트 | 입력 | 예상 결과 | 실제 결과 | 상태 |
|
||
|
|
|-----------|------|----------|----------|------|
|
||
|
|
| GET /biddings | - | 목록 반환 | | ⏳ |
|
||
|
|
| GET /biddings/stats | - | 통계 반환 | | ⏳ |
|
||
|
|
| GET /biddings/{id} | id=1 | 단건 반환 | | ⏳ |
|
||
|
|
| PUT /biddings/{id} | 수정 데이터 | 수정 성공 | | ⏳ |
|
||
|
|
| POST /quotes/{id}/convert-to-bidding | quote_id | 입찰 생성 | | ⏳ |
|
||
|
|
|
||
|
|
### 9.2 성공 기준 달성 현황
|
||
|
|
|
||
|
|
| 기준 | 달성 | 비고 |
|
||
|
|
|------|------|------|
|
||
|
|
| Bidding API CRUD 동작 | ⏳ | |
|
||
|
|
| 견적 → 입찰 전환 동작 | ⏳ | |
|
||
|
|
| 더미데이터 10건 생성 | ⏳ | |
|
||
|
|
| Swagger 문서 완성 | ⏳ | |
|
||
|
|
| Pint 통과 | ⏳ | |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. 자기완결성 점검 결과
|
||
|
|
|
||
|
|
### 10.1 체크리스트 검증
|
||
|
|
|
||
|
|
| # | 검증 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 1 | 작업 목적이 명확한가? | ✅ | 견적→입찰 전환 + 더미데이터 |
|
||
|
|
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 참조 |
|
||
|
|
| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-4 정의 |
|
||
|
|
| 4 | 의존성이 명시되어 있는가? | ✅ | quotes API 의존 |
|
||
|
|
| 5 | 참고 파일 경로가 정확한가? | ✅ | 7. 참고 문서 |
|
||
|
|
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.1 절차 |
|
||
|
|
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 |
|
||
|
|
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/API 명시 |
|
||
|
|
|
||
|
|
### 10.2 새 세션 시뮬레이션 테스트
|
||
|
|
|
||
|
|
| 질문 | 답변 가능 | 참조 섹션 |
|
||
|
|
|------|:--------:|----------|
|
||
|
|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||
|
|
| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태 + 3.1 |
|
||
|
|
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 |
|
||
|
|
| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
|
||
|
|
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |
|
||
|
|
|
||
|
|
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|