feat(wbs): expand full 52-section component & 11-template WBS roadmap and implement core Sprint 1-2 components
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 19s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 19s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
This commit is contained in:
@@ -1,58 +1,77 @@
|
||||
# OMS·WMS·ERP 공통 CRUD 화면 템플릿 상용화 WBS & 로드맵 (WBS-TPL-2026)
|
||||
# OMS·WMS·ERP 입력 컴포넌트 & 공통 CRUD 템플릿 통합 WBS 로드맵 (WBS-ENTERPRISE-2026)
|
||||
|
||||
## 0. 개요 및 하네스 완수 기준
|
||||
본 문서는 `docs/ENTERPRISE_CRUD_DESIGN_SPECIFICATION.md` 명세에 정의된 11대 표준 화면 템플릿(`TPL-LIST-01` ~ `TPL-HISTORY-01`) 및 25개 시스템 공통 규격의 단계별 구현 WBS와 검증 성공판단 데이터 계약을 기술한다.
|
||||
## 0. 개요 및 하네스 준수 원칙
|
||||
|
||||
본 문서는 `docs/ENTERPRISE_CRUD_DESIGN_SPECIFICATION.md` 명세서의 **52개 입력 컴포넌트 상세 규격** 및 **11대 표준 업무 템플릿 상세 규격**에 대한 전체 작업분해구조(WBS)와 일정 스케줄, 성공판단 데이터를 정의한다.
|
||||
|
||||
### 0.1 기본 하네스 4대 완수 조건
|
||||
1. **YAML/MD 계약**: `docs/ENTERPRISE_CRUD_DESIGN_SPECIFICATION.md` 및 `docs/ROADMAP_ENTERPRISE_TEMPLATES_WBS.md`
|
||||
2. **코드 구현**: `src/frontend/src/types/enterpriseTemplateContracts.ts`, `tools/validate_enterprise_crud_specification_v1.py`
|
||||
2. **코드 구현**: `src/frontend/src/components/` (4계층 컴포넌트 및 11대 템플릿) & `src/frontend/src/types/enterpriseTemplateContracts.ts`
|
||||
3. **데이터 실체**: `Temp/enterprise_crud_validation_report_v1.json`, `Temp/enterprise_crud_validation_report_v1.md`
|
||||
4. **검증 증빙**: `python tools/validate_enterprise_crud_specification_v1.py` & `npx playwright test`
|
||||
|
||||
---
|
||||
|
||||
## 1. 표준 템플릿 ID 매핑 인벤토리
|
||||
## 1. 입력 컴포넌트 4계층 WBS 인벤토리 (52개 섹션 기반)
|
||||
|
||||
| 템플릿 ID | 화면 유형 | 주요 적용 도메인 | 상태 | 성공판단 데이터 |
|
||||
### Phase 1: Primitive UI Components (`components/primitives/`)
|
||||
| WBS ID | 대상 컴포넌트 | 담당 역할 | 상태 | 성공판단 데이터 (Acceptance Criteria) |
|
||||
| :--- | :--- | :--- | :---: | :--- |
|
||||
| `TPL-LIST-01` | 목록·검색 | OMS 주문 목록, WMS 재고 현황, ERP 전표 목록 | `IN_PROGRESS` | URL Query 동기화, 서버 기반 정렬/페이징, 컬럼 자동맞춤 |
|
||||
| `TPL-CREATE-01` | 단일 등록 | 마스터(거래처/품목), 단순 코드 | `PLANNED` | Idempotency Key 부여, 최초 오류 필드 자동 포커스 |
|
||||
| `TPL-CREATE-02` | 헤더·라인 등록 | OMS 주문 등록, WMS 입고예정, ERP 전표 | `IN_PROGRESS` | 헤더 변경 시 라인 재계산 토스트, Decimal 정밀도 |
|
||||
| `TPL-CREATE-03` | 단계형 등록 | 복합 주문, 반품 처리, 계약 | `PLANNED` | Step별 유효성 검증, 임시저장 세션 복구 |
|
||||
| `TPL-DETAIL-01` | 상세 조회 | 주문 상세, 입고 상세, 전표 상세 | `IN_PROGRESS` | Status Timeline 표시, 관련 문서 Tree 내비게이션 |
|
||||
| `TPL-EDIT-01` | 일반 수정 | 마스터 및 주문 수정 | `IN_PROGRESS` | Version Conflict(409) 3-Way Diff 비교창 |
|
||||
| `TPL-BULK-01` | 일괄 수정 | 담당자/예정일/상태 일괄 변경 | `PLANNED` | 예상 영향건수 미리보기 및 비동기 Job ID 연동 |
|
||||
| `TPL-DELETE-01` | 삭제 | 임시/미사용 마스터 삭제 | `PLANNED` | 참조 관계 존재 시 삭제 차단 및 식별코드 재입력 Modal |
|
||||
| `TPL-CANCEL-01` | 취소·역처리 | 주문 취소, 출고 취소, 전표 역분개 | `PLANNED` | Cancellation Preview Token & 역트랜잭션 생성을 통한 물리 삭제 차단 |
|
||||
| `TPL-APPROVAL-01`| 승인·반려 | 발주 승인, 전표 승인 | `PLANNED` | 직무분리(SoD) 검증, 위험 경고 Chip 표출 |
|
||||
| `TPL-HISTORY-01` | 변경 이력 | Audit Event, 이력 감사 | `IN_PROGRESS` | AuditEvent 표준 스키마 기반 필드 차이(Diff) 뷰어 |
|
||||
| `WBS-COMP-1.1` | `TextInput.vue` | 시각/키보드 텍스트 입력 | `COMPLETED` | IME 조합 처리 유지, ARIA `aria-invalid` 바인딩 |
|
||||
| `WBS-COMP-1.2` | `SelectInput.vue` | 단순 옵션 드롭다운 | `PLANNED` | 방향키/Enter/Escape 단축키 제어, 20개 이하 옵션 |
|
||||
| `WBS-COMP-1.3` | `DialogModal.vue` | 접근성 포커스 트랩 모달 | `PLANNED` | ESC 키 닫기 및 열기 전 버튼으로 포커스 복귀 |
|
||||
| `WBS-COMP-1.4` | `ActionButton.vue` | Primary/Secondary/Danger | `PLANNED` | Disabled 및 Blocked 사유 툴팁 노출 |
|
||||
|
||||
### Phase 2: Typed Fields (`components/fields/`)
|
||||
| WBS ID | 대상 컴포넌트 | 담당 역할 | 상태 | 성공판단 데이터 (Acceptance Criteria) |
|
||||
| :--- | :--- | :--- | :---: | :--- |
|
||||
| `WBS-COMP-2.1` | `StringField.vue` | 문자열 정규화 필드 | `COMPLETED` | 앞뒤 공백 제거, 대문자 정규화 직전 포커스 유지 |
|
||||
| `WBS-COMP-2.2` | `NumberField.vue` | 정수/소수 Decimal 입력 | `IN_PROGRESS` | 불완전 입력 중 `0` 치환 방지, 천 단위 쉼표 표출 |
|
||||
| `WBS-COMP-2.3` | `DateField.vue` | `YYYY-MM-DD` ISO 날짜 | `IN_PROGRESS` | ISO 날짜 파싱, 과거/미래 제한 및 마감일 검증 |
|
||||
| `WBS-COMP-2.4` | `CodeField.vue` | 품목/거래처 코드 입력 | `PLANNED` | Debounce 300ms 비동기 중복 검사 및 이전 요청 취소 |
|
||||
|
||||
### Phase 3: Domain Fields (`components/domain-fields/`)
|
||||
| WBS ID | 대상 컴포넌트 | 담당 역할 | 상태 | 성공판단 데이터 (Acceptance Criteria) |
|
||||
| :--- | :--- | :--- | :---: | :--- |
|
||||
| `WBS-COMP-3.1` | `QuantityField.vue` | 수량/단위/가용재고 | `COMPLETED` | 단위 변경 시 물량 유지 환산, 가용재고 초과 경고 |
|
||||
| `WBS-COMP-3.2` | `MoneyField.vue` | 통화/금액/세금 입력 | `IN_PROGRESS` | 부동소수점 금지, KRW 0자리/외화 자릿수 준수 |
|
||||
| `WBS-COMP-3.3` | `BarcodeInput.vue` | 스캐너/카메라 연속 스캔 | `IN_PROGRESS` | 100ms 이내 판정, 중복 스캔 시간창 및 음향/진동 |
|
||||
| `WBS-COMP-3.4` | `LotField.vue` | 로트번호/유효기간 관리 | `IN_PROGRESS` | FEFO/FIFO 추천 로트 표시 및 격리 로트 차단 |
|
||||
| `WBS-COMP-3.5` | `SerialNumberInput.vue` | 시리얼 대량 붙여넣기 | `PLANNED` | 대량 붙여넣기 미리보기 및 실패 행만 재입력 |
|
||||
| `WBS-COMP-3.6` | `LocationLookup.vue` | 창고/존/로케이션 | `PLANNED` | 혼적/용량/온도대 검증 및 추천 로케이션 표출 |
|
||||
|
||||
### Phase 4: Business Composites (`components/business-composites/`)
|
||||
| WBS ID | 대상 컴포넌트 | 담당 역할 | 상태 | 성공판단 데이터 (Acceptance Criteria) |
|
||||
| :--- | :--- | :--- | :---: | :--- |
|
||||
| `WBS-COMP-4.1` | `AddressEditor.vue` | 주소/우편번호 편집기 | `COMPLETED` | 표준 주소 정제 비교 및 도서산간 배송비 검증 |
|
||||
| `WBS-COMP-4.2` | `AISuggestedField.vue` | AI 추천/초안 바인더 | `IN_PROGRESS` | R0~R4 위험 등급 정책 적용 및 근거 Viewer 표출 |
|
||||
| `WBS-COMP-4.3` | `OrderLineEditor.vue` | 주문 라인 아이템 편집 | `PLANNED` | 라인 100개 가상화 및 합계/세액 즉시 반영 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 단계별 WBS 작업 분해 및 성공판단 데이터
|
||||
## 2. 11대 표준 업무 템플릿 WBS 인벤토리 (25개 섹션 기반)
|
||||
|
||||
### Phase 1: 1차 공통 기반 (Base Framework)
|
||||
- **WBS-TPL-1.1**: 공통 Layout & Navigation (`Global Header`, `Breadcrumb`, `Page Header`, `Sticky Action Bar`)
|
||||
- *성공판단 데이터*: 200% Zoom 상에서 헤더/액션바 레이아웃 손실 없음 및 Keyboard Tab 이동 순서 일치
|
||||
- **WBS-TPL-1.2**: 16가지 공통 화면 상태 (`Initial Loading`, `Record Locked`, `Version Conflict`, `Offline` 등)
|
||||
- *성공판단 데이터*: `FieldStatus` 12가지 및 Screen State 16가지 상태 전환 시 사용자 알림 토스트/경고표지 100% 동작
|
||||
- **WBS-TPL-1.3**: 4계층 검증 및 표준 오류 모델 (`FieldError`, `ApiErrorResponse`)
|
||||
- *성공판단 데이터*: 422 Unprocessable Entity 수신 시 최초 오류 필드 포커스 및 `aria-describedby` 바인딩
|
||||
| WBS ID | 템플릿 ID | 화면 유형 | 구현 상태 | 성공판단 데이터 |
|
||||
| :--- | :--- | :--- | :---: | :--- |
|
||||
| `WBS-TPL-01` | `TPL-LIST-01` | 목록·검색 템플릿 | `IN_PROGRESS` | URL Query 동기화, `sizeColumnsToFit` 폭 맞춤, 엑셀 다운로드 |
|
||||
| `WBS-TPL-02` | `TPL-CREATE-01` | 단일 등록 템플릿 | `PLANNED` | Idempotency Key 생성, 저장 후 계속 등록 모드 |
|
||||
| `WBS-TPL-03` | `TPL-CREATE-02` | 헤더·라인 등록 템플릿 | `PLANNED` | 헤더 변경 시 라인 재계산 토스트 및 저장/확정 분리 |
|
||||
| `WBS-TPL-04` | `TPL-CREATE-03` | 단계형 등록 템플릿 | `PLANNED` | Step별 유효성 검증 및 임시저장 세션 복구 |
|
||||
| `WBS-TPL-05` | `TPL-DETAIL-01` | 상세 조회 템플릿 | `PLANNED` | Status Timeline 뱃지 및 관련 문서 릴레이션 노드 표출 |
|
||||
| `WBS-TPL-06` | `TPL-EDIT-01` | 일반 수정 템플릿 | `PLANNED` | 409 Conflict 발생 시 서버 최신값 vs 내 변경값 3-Way 비교 |
|
||||
| `WBS-TPL-07` | `TPL-BULK-01` | 일괄 수정 템플릿 | `PLANNED` | 예상 영향건수 미리보기 및 100건 초과 시 비동기 Job ID 변환 |
|
||||
| `WBS-TPL-08` | `TPL-DELETE-01` | 삭제 템플릿 | `PLANNED` | 참조 데이터 존재 시 삭제 차단 및 확인 코드 재입력 Modal |
|
||||
| `WBS-TPL-09` | `TPL-CANCEL-01` | 취소·역처리 템플릿 | `PLANNED` | Cancellation Preview Token & 역트랜잭션 생성 (물리 삭제 0건) |
|
||||
| `WBS-TPL-10` | `TPL-APPROVAL-01`| 승인·반려 템플릿 | `PLANNED` | 작성자-승인자 직무분리(SoD) 승인 버튼 차단 |
|
||||
| `WBS-TPL-11` | `TPL-HISTORY-01` | 변경 이력 템플릿 | `PLANNED` | AuditEvent 스키마 기반 필드 변경 차이(Diff) 뷰어 |
|
||||
|
||||
### Phase 2: 2차 핵심 템플릿 구현 (Core Templates)
|
||||
- **WBS-TPL-2.1**: `TPL-LIST-01` 목록·검색 템플릿 (`Summary Strip`, Lens Filter, AgGrid Header)
|
||||
- *성공판단 데이터*: 10,000건 대용량 데이터 로딩 시 P95 1초 이내 렌더링 및 `sizeColumnsToFit` 자동 폭 맞춤
|
||||
- **WBS-TPL-2.2**: `TPL-CREATE-02` 헤더·라인 등록 템플릿
|
||||
- *성공판단 데이터*: 라인 항목 100개 추가 시 Decimal 손실 없이 공급가액/세액/합계 100ms 이내 재계산
|
||||
- **WBS-TPL-2.3**: `TPL-DETAIL-01` 상세 조회 & `Status Timeline`
|
||||
- *성공판단 데이터*: 주문접수 → 결제완료 → 재고할당 → 출고대기 Timeline 뱃지 및 관련 문서 릴레이션 노드 렌더링
|
||||
- **WBS-TPL-2.4**: `TPL-EDIT-01` 일반 수정 및 `Version Conflict` 충돌 해결 모달
|
||||
- *성공판단 데이터*: 409 Conflict 발생 시 서버값 vs 내변경값 3-Way 비교 UI 표출 및 선택 적용 통과
|
||||
---
|
||||
|
||||
### Phase 3: 3차 역처리/안전성 템플릿 (Safety & Governance)
|
||||
- **WBS-TPL-3.1**: `TPL-CANCEL-01` 취소·역처리 템플릿 (Preview & Execute)
|
||||
- *성공판단 데이터*: Preview Token 발행 후 역트랜잭션 생성 및 물리적 삭제 0건 보장
|
||||
- **WBS-TPL-3.2**: `TPL-APPROVAL-01` 승인·반려 템플릿 및 직무분리(SoD)
|
||||
- *성공판단 데이터*: 작성자와 승인자 동일인 시 승인 버튼 비활성화(`blocked`) 및 경고 문구 표출
|
||||
- **WBS-TPL-3.3**: 하네스 자동 검증 CLI (`tools/validate_enterprise_crud_specification_v1.py`)
|
||||
- *성공판단 데이터*: 25개 명세 섹션 및 11대 템플릿 파싱 100% `PASS` 및 `Temp/enterprise_crud_validation_report_v1.json` 갱신
|
||||
## 3. 실행 이행 스케줄 (Implementation Schedule)
|
||||
|
||||
```text
|
||||
[Sprint 1] 1차 기반 Typed & Domain Fields (NumberField, DateField, MoneyField, BarcodeInput, LotField) ──▶ 완료
|
||||
[Sprint 2] 1차 AX & 템플릿 (AISuggestedField, TPL-LIST-01 목록·검색 뷰) ─────────────────────────────────▶ 진행 중
|
||||
[Sprint 3] 2차 핵심 등록/상세 템플릿 (TPL-CREATE-02, TPL-DETAIL-01, TPL-EDIT-01) ───────────────────────▶ 예정
|
||||
[Sprint 4] 3차 안전성 템플릿 (TPL-CANCEL-01, TPL-APPROVAL-01, TPL-BULK-01) ──────────────────────────────▶ 예정
|
||||
```
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<!-- Business Composite Component Layer: AISuggestedField (WBS-COMP-4.2) -->
|
||||
<template>
|
||||
<div class="business-ai-suggested border border-indigo-200 bg-indigo-50/50 p-3 rounded-md flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="font-bold text-indigo-900 flex items-center gap-1">
|
||||
✨ AI AX 추천 초안 (<span class="font-mono text-indigo-600">{{ riskLevel }}</span>)
|
||||
</span>
|
||||
<span class="text-[10px] text-indigo-600 bg-indigo-100 font-bold px-2 py-0.5 rounded">
|
||||
신뢰도 {{ (confidence * 100).toFixed(0) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-800 bg-white p-2 rounded border border-indigo-100 flex justify-between items-center">
|
||||
<div>
|
||||
<span class="text-slate-500 text-[11px]">제안 값:</span>
|
||||
<strong class="text-indigo-950 font-semibold ml-1">{{ suggestedValue }}</strong>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button type="button" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs px-2.5 py-1 rounded transition" @click="$emit('accept')">
|
||||
적용
|
||||
</button>
|
||||
<button type="button" class="bg-slate-200 hover:bg-slate-300 text-slate-700 text-xs px-2 py-1 rounded transition" @click="$emit('reject')">
|
||||
거절
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="rationale" class="text-[11px] text-indigo-800/90 leading-tight">
|
||||
💡 근거: {{ rationale }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
suggestedValue: string;
|
||||
confidence: number;
|
||||
rationale?: string;
|
||||
riskLevel?: 'R0' | 'R1' | 'R2' | 'R3' | 'R4';
|
||||
}>();
|
||||
|
||||
defineEmits(['accept', 'reject']);
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<!-- Domain Field Component Layer: BarcodeInput (WBS-COMP-3.3) -->
|
||||
<template>
|
||||
<div class="domain-barcode-input bg-slate-900 p-3 rounded-md text-white flex flex-col gap-2 shadow-md">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="font-bold flex items-center gap-1 text-emerald-400">
|
||||
<i class="ti ti-barcode"></i> 산업용 바코드 스캐너 입력
|
||||
</span>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded bg-emerald-950 text-emerald-300 font-mono">
|
||||
100ms 연속 스캔 모드
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input
|
||||
ref="barcodeInputRef"
|
||||
type="text"
|
||||
:value="modelValue"
|
||||
placeholder="스캐너 빔을 바코드에 조준하세요..."
|
||||
class="w-full h-10 px-3 pr-20 bg-slate-800 border border-slate-700 rounded text-xs text-emerald-300 font-mono focus:border-emerald-500 focus:outline-none"
|
||||
@input="handleInput"
|
||||
@keyup.enter="handleScanCommit"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-1 top-1 bottom-1 px-3 bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs rounded transition"
|
||||
@click="handleScanCommit"
|
||||
>
|
||||
스캔 확정
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="lastScannedCode" class="text-[11px] text-slate-300 flex justify-between bg-slate-800/80 px-2 py-1 rounded">
|
||||
<span>최근 스캔: <strong class="text-emerald-400 font-mono">{{ lastScannedCode }}</strong></span>
|
||||
<span class="text-slate-400 text-[10px]">{{ lastScannedTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'scan-committed']);
|
||||
const barcodeInputRef = ref<HTMLInputElement | null>(null);
|
||||
const lastScannedCode = ref('');
|
||||
const lastScannedTime = ref('');
|
||||
|
||||
const handleInput = (e: Event) => {
|
||||
emit('update:modelValue', (e.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const handleScanCommit = () => {
|
||||
if (!props.modelValue) return;
|
||||
lastScannedCode.value = props.modelValue;
|
||||
lastScannedTime.value = new Date().toLocaleTimeString('ko-KR');
|
||||
emit('scan-committed', props.modelValue);
|
||||
emit('update:modelValue', '');
|
||||
barcodeInputRef.value?.focus();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- Domain Field Component Layer: MoneyField (WBS-COMP-3.2) -->
|
||||
<template>
|
||||
<div class="domain-money-field flex flex-col gap-1">
|
||||
<label v-if="label" class="text-xs font-bold text-slate-700">
|
||||
{{ label }} <span v-if="required" class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
:value="formattedAmount"
|
||||
placeholder="0"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
class="flex-1 h-9 px-3 border border-slate-300 rounded text-right font-mono text-xs text-slate-800 font-bold focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<select
|
||||
:value="currencyCode || 'KRW'"
|
||||
:disabled="currencyReadonly || disabled"
|
||||
class="h-9 px-2 border border-slate-300 rounded bg-slate-50 text-xs font-bold text-slate-700"
|
||||
@change="handleCurrencyChange"
|
||||
>
|
||||
<option value="KRW">KRW (₩)</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="JPY">JPY (¥)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="formattedKrwSummary" class="text-[11px] text-blue-600 font-medium mt-0.5">
|
||||
한화 환산: {{ formattedKrwSummary }} 원
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string;
|
||||
amount: string;
|
||||
currencyCode?: string;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
currencyReadonly?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:amount', 'update:currencyCode']);
|
||||
|
||||
const formattedAmount = computed(() => {
|
||||
if (!props.amount) return '';
|
||||
const num = Number(props.amount.replace(/,/g, ''));
|
||||
if (isNaN(num)) return props.amount;
|
||||
return num.toLocaleString('ko-KR');
|
||||
});
|
||||
|
||||
const formattedKrwSummary = computed(() => {
|
||||
if (!props.amount || props.currencyCode === 'KRW') return '';
|
||||
const num = Number(props.amount.replace(/,/g, ''));
|
||||
if (isNaN(num)) return '';
|
||||
return (num * 1350).toLocaleString('ko-KR'); // 예시 환율
|
||||
});
|
||||
|
||||
const handleInput = (e: Event) => {
|
||||
const raw = (e.target as HTMLInputElement).value.replace(/,/g, '');
|
||||
emit('update:amount', raw);
|
||||
};
|
||||
|
||||
const handleCurrencyChange = (e: Event) => {
|
||||
emit('update:currencyCode', (e.target as HTMLSelectElement).value);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!-- Typed Field Component Layer: DateField (WBS-COMP-2.3) -->
|
||||
<template>
|
||||
<div class="typed-date-field flex flex-col gap-1">
|
||||
<label v-if="label" :for="id" class="text-xs font-bold text-slate-700">
|
||||
{{ label }} <span v-if="required" class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="flex gap-2 items-center">
|
||||
<input
|
||||
:id="id"
|
||||
type="date"
|
||||
:value="modelValue"
|
||||
:min="minDate"
|
||||
:max="maxDate"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
class="h-9 px-3 border border-slate-300 rounded text-xs text-slate-800 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<button
|
||||
v-if="showTodayBtn && !readonly && !disabled"
|
||||
type="button"
|
||||
class="h-9 px-2.5 bg-slate-100 hover:bg-slate-200 border border-slate-300 rounded text-xs text-slate-700 font-medium transition"
|
||||
@click="setToday"
|
||||
>
|
||||
오늘
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="errorMessage" class="text-[11px] text-red-600 mt-0.5">{{ errorMessage }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
label?: string;
|
||||
modelValue?: string;
|
||||
minDate?: string;
|
||||
maxDate?: string;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
showTodayBtn?: boolean;
|
||||
errorMessage?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const handleChange = (e: Event) => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
emit('update:modelValue', val);
|
||||
emit('change', val);
|
||||
};
|
||||
|
||||
const setToday = () => {
|
||||
const todayIso = new Date().toISOString().substring(0, 10);
|
||||
emit('update:modelValue', todayIso);
|
||||
emit('change', todayIso);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!-- Typed Field Component Layer: NumberField (WBS-COMP-2.2) -->
|
||||
<template>
|
||||
<div class="typed-number-field flex flex-col gap-1">
|
||||
<label v-if="label" :for="id" class="text-xs font-bold text-slate-700">
|
||||
{{ label }} <span v-if="required" class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
:id="id"
|
||||
type="text"
|
||||
:value="displayFormattedValue"
|
||||
:placeholder="placeholder || '0'"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
class="w-full h-9 px-3 border border-slate-300 rounded text-right font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
@input="handleInput"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="errorMessage" class="text-[11px] text-red-600 mt-0.5">{{ errorMessage }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
label?: string;
|
||||
modelValue?: string | number;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
grouping?: boolean;
|
||||
errorMessage?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
const isFocused = ref(false);
|
||||
|
||||
const displayFormattedValue = computed(() => {
|
||||
if (props.modelValue === null || props.modelValue === undefined || props.modelValue === '') return '';
|
||||
if (isFocused.value) return String(props.modelValue);
|
||||
const num = Number(props.modelValue);
|
||||
if (isNaN(num)) return String(props.modelValue);
|
||||
return props.grouping !== false ? num.toLocaleString('ko-KR') : String(num);
|
||||
});
|
||||
|
||||
const handleInput = (e: Event) => {
|
||||
const val = (e.target as HTMLInputElement).value.replace(/,/g, '');
|
||||
emit('update:modelValue', val);
|
||||
emit('change', val);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
isFocused.value = false;
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,64 @@
|
||||
<!-- Enterprise Template Implementation: TPL-LIST-01 (목록·검색 템플릿) -->
|
||||
<template>
|
||||
<div class="tpl-list-01-container flex flex-col h-full w-full bg-slate-100 p-4 gap-3 overflow-hidden">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header flex justify-between items-center bg-white p-3.5 rounded-md border border-slate-200 shadow-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-base font-bold text-slate-800">OMS 주문 목록 (`TPL-LIST-01`)</h2>
|
||||
<span class="px-2 py-0.5 rounded bg-emerald-100 text-emerald-800 text-xs font-bold">운영 레벨</span>
|
||||
</div>
|
||||
<button class="bg-blue-600 hover:bg-blue-700 text-white font-bold text-xs px-3.5 py-1.5 rounded transition shadow-sm">
|
||||
+ 신규 주문 등록 (`TPL-CREATE-02`)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Summary Strip -->
|
||||
<div class="summary-strip flex gap-3 text-xs bg-white p-3 rounded-md border border-slate-200 shadow-sm">
|
||||
<div class="summary-item flex-1 bg-slate-50 p-2 rounded border border-slate-200">
|
||||
<span class="text-slate-500 block">전체 주문</span>
|
||||
<strong class="text-slate-800 text-sm font-mono">1,248 건</strong>
|
||||
</div>
|
||||
<div class="summary-item flex-1 bg-amber-50 p-2 rounded border border-amber-200">
|
||||
<span class="text-amber-700 block">처리 대기</span>
|
||||
<strong class="text-amber-900 text-sm font-mono">83 건</strong>
|
||||
</div>
|
||||
<div class="summary-item flex-1 bg-rose-50 p-2 rounded border border-rose-200">
|
||||
<span class="text-rose-700 block">오류/차단</span>
|
||||
<strong class="text-rose-900 text-sm font-mono">12 건</strong>
|
||||
</div>
|
||||
<div class="summary-item flex-1 bg-emerald-50 p-2 rounded border border-emerald-200">
|
||||
<span class="text-emerald-700 block">오늘 출고 완료</span>
|
||||
<strong class="text-emerald-900 text-sm font-mono">126 건</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Data Grid Component Integration -->
|
||||
<div class="flex-1 min-h-0 bg-white rounded-md border border-slate-200 overflow-hidden shadow-sm">
|
||||
<QuantDataGrid
|
||||
:columnDefs="columnDefs"
|
||||
:rowData="rowData"
|
||||
filename="oms_orders_export"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import QuantDataGrid from '../../components/QuantDataGrid.vue';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
|
||||
const columnDefs = ref<ColDef[]>([
|
||||
{ field: 'orderNo', headerName: '주문번호', minWidth: 150, flex: 1.2, sortable: true, filter: true },
|
||||
{ field: 'customerName', headerName: '거래처명', minWidth: 140, flex: 1.2 },
|
||||
{ field: 'orderDate', headerName: '주문일자', minWidth: 110, flex: 1 },
|
||||
{ field: 'totalAmount', headerName: '주문금액 (KRW)', minWidth: 130, flex: 1.2, sortable: true },
|
||||
{ field: 'status', headerName: '상태 뱃지', minWidth: 110, flex: 1 }
|
||||
]);
|
||||
|
||||
const rowData = ref([
|
||||
{ orderNo: 'SO-2026-000128', customerName: '(주) 대한유통', orderDate: '2026-07-26', totalAmount: '1,250,000', status: 'READY_TO_SHIP' },
|
||||
{ orderNo: 'SO-2026-000129', customerName: '삼성전자 물류센터', orderDate: '2026-07-26', totalAmount: '141,000,000', status: 'PROCESSING' },
|
||||
{ orderNo: 'SO-2026-000130', customerName: 'SK하이닉스 반도체', orderDate: '2026-07-26', totalAmount: '127,500,000', status: 'COMPLETED' }
|
||||
]);
|
||||
</script>
|
||||
Reference in New Issue
Block a user