Merge branch 'jskim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
# Plan Review Report: API 로직 전체 복구 + 테이블 설정 인라인 구현
|
||||||
|
|
||||||
|
### 1. 플랜 요약
|
||||||
|
COMPANY_7 원본 기준으로 COMPANY_16의 전 화면(43개 파일, 20개 태스크) 데이터 흐름을 동기화하고, 해당 화면에 테이블 설정 기능을 인라인 구현.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 문제점 지적
|
||||||
|
|
||||||
|
#### 🔴 수정 필요: ref_files 12개 미존재
|
||||||
|
|
||||||
|
다음 COMPANY_7 원본 파일이 존재하지 않아 에이전트가 참조할 수 없습니다:
|
||||||
|
|
||||||
|
| 태스크 | 누락된 ref_file |
|
||||||
|
|--------|----------------|
|
||||||
|
| task-10 (BOM) | `COMPANY_7/production/bom/page.tsx` |
|
||||||
|
| task-11a (발주) | `COMPANY_7/purchase/order/page.tsx` |
|
||||||
|
| task-11b (구매품목) | `COMPANY_7/purchase/purchase-item/page.tsx` |
|
||||||
|
| task-11b (공급업체) | `COMPANY_7/purchase/supplier/page.tsx` |
|
||||||
|
| task-13b (재고) | `COMPANY_7/logistics/inventory/page.tsx` |
|
||||||
|
| task-13b (창고) | `COMPANY_7/logistics/warehouse/page.tsx` |
|
||||||
|
| task-13b (물류정보) | `COMPANY_7/logistics/info/page.tsx` |
|
||||||
|
| task-14 (금형) | `COMPANY_7/mold/info/page.tsx` |
|
||||||
|
| task-15a (회사) | `COMPANY_7/master-data/company/page.tsx` |
|
||||||
|
| task-15b (검사) | `COMPANY_7/quality/inspection/page.tsx` |
|
||||||
|
| task-15b (품목검사) | `COMPANY_7/quality/item-inspection/page.tsx` |
|
||||||
|
| task-15b (PLC) | `COMPANY_7/equipment/plc-settings/page.tsx` |
|
||||||
|
|
||||||
|
→ 이 12개 ref_file이 없으면 에이전트는 "COMPANY_7 기준으로 동기화"라는 지시를 수행할 수 없습니다. **플랜 실행 전 반드시 해결 필요.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 🟠 도주 위험
|
||||||
|
|
||||||
|
**task-11a (발주관리)**: context가 "수주관리와 동일 패턴"으로 5줄. ref_file도 누락. 에이전트가 최소 수정만 하고 "완료" 보고할 가능성 높음.
|
||||||
|
|
||||||
|
**task-11b (구매품목+공급업체)**: 2개 파일인데 context가 "판매품목과 동일 패턴"/"거래처관리와 동일 패턴" 한 줄씩. ref_file도 둘 다 누락. 이중 위험.
|
||||||
|
|
||||||
|
**task-17a/17b (리포트)**: "이미 이전 파이프라인에서 수정됨. 누락분 보강" → 에이전트가 "확인 결과 이미 충족" 판단 후 아무것도 안 할 가능성. ref_files가 자기 자신(files == ref_files)이므로 비교 대상이 없음.
|
||||||
|
|
||||||
|
**task-14 (외주+설비+금형)**: 4개 파일인데 각 파일별 context가 한 줄. 금형은 ref_file도 누락.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 🟡 충돌 감지
|
||||||
|
|
||||||
|
파일 겹침은 없습니다. 모든 태스크의 files가 고유합니다. depends도 전부 none으로 순환 없음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 수정 범위 예상
|
||||||
|
|
||||||
|
| 항목 | 수치 |
|
||||||
|
|------|------|
|
||||||
|
| 총 태스크 수 | 20개 |
|
||||||
|
| 대상 파일 수 | 43개 |
|
||||||
|
| ref_file 존재 | 60개 중 48개 (12개 누락) |
|
||||||
|
| 예상 변경 줄 수 | 태스크당 200~800줄 × 20 = **4,000~16,000줄** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 예상 구동시간
|
||||||
|
|
||||||
|
- 20 태스크, max_concurrent: 5 → **4 웨이브**
|
||||||
|
- timeout: 30m/태스크
|
||||||
|
- 최악: 4 × 30m = **2시간**
|
||||||
|
- 현실적: 대부분 1~2파일 태스크 → **1~1.5시간**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 검증 단계 확인
|
||||||
|
|
||||||
|
| 검증 | 현재 상태 | 비고 |
|
||||||
|
|------|----------|------|
|
||||||
|
| L1 (tsc --noEmit) | ✅ 전 태스크 설정됨 | 양호 |
|
||||||
|
| L6 (verify/grep) | ✅ 전 태스크 설정됨 | 대부분 주요 함수명/패턴 grep |
|
||||||
|
| L3 (api_test) | ✅ 전 태스크 설정됨 | 실제 API 호출로 검증 |
|
||||||
|
|
||||||
|
verify 품질: task-1은 3개 패턴만 체크(dataFilter, autoFilter, tableSettings)로 다소 약함. [A]~[I] 전체 로직 반영 대비 부족하나, api_test가 보완하므로 수용 가능.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 종합 판단
|
||||||
|
|
||||||
|
**ref_files 12개 누락이 가장 큰 블로커입니다.** 이 파일들 없이 실행하면 해당 7개 태스크(task-10, 11a, 11b, 13b, 14, 15a, 15b)가 "참조할 원본 없이 추측 수정"하게 됩니다.
|
||||||
|
|
||||||
|
**추천 조치:**
|
||||||
|
1. 누락 ref_files → COMPANY_7에 해당 파일 생성하거나, 다른 경로에 있다면 경로 수정
|
||||||
|
2. task-11a/11b → context에 task-1/task-2 수준의 상세 로직(useState 목록, API 파라미터, 저장/삭제 패턴) 추가
|
||||||
|
3. task-17a/17b → ref_files를 자기 자신이 아닌 정답 기준 파일로 교체하거나, context에 "최소 diff N줄" 기준 추가
|
||||||
|
4. task-14 금형 → ref_file 없이 수행 가능하도록 context에 전용 API 전체 명세 포함 필요
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: pipeline-backend
|
||||||
|
description: Agent Pipeline 백엔드 전문가. Express + TypeScript + PostgreSQL Raw Query 기반 API 구현. 멀티테넌시(company_code) 필터링 필수.
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role
|
||||||
|
You are a Backend specialist for ERP-node project.
|
||||||
|
Stack: Node.js + Express + TypeScript + PostgreSQL Raw Query.
|
||||||
|
|
||||||
|
# CRITICAL PROJECT RULES
|
||||||
|
|
||||||
|
## 1. Multi-tenancy (ABSOLUTE MUST!)
|
||||||
|
- ALL queries MUST include company_code filter
|
||||||
|
- Use req.user!.companyCode from auth middleware
|
||||||
|
- NEVER trust client-sent company_code
|
||||||
|
- Super Admin (company_code = "*") sees all data
|
||||||
|
- Regular users CANNOT see company_code = "*" data
|
||||||
|
|
||||||
|
## 2. Required Code Pattern
|
||||||
|
```typescript
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
if (companyCode === "*") {
|
||||||
|
query = "SELECT * FROM table ORDER BY company_code";
|
||||||
|
} else {
|
||||||
|
query = "SELECT * FROM table WHERE company_code = $1 AND company_code != '*'";
|
||||||
|
params = [companyCode];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Controller Structure
|
||||||
|
```typescript
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import pool from "../config/database";
|
||||||
|
import { logger } from "../config/logger";
|
||||||
|
|
||||||
|
export const getList = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
// ... company_code 분기 처리
|
||||||
|
const result = await pool.query(query, params);
|
||||||
|
res.json({ success: true, data: result.rows });
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error("조회 실패", error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Route Registration
|
||||||
|
- backend-node/src/routes/index.ts에 import 추가 필수
|
||||||
|
- authenticateToken 미들웨어 적용 필수
|
||||||
|
|
||||||
|
# Your Domain
|
||||||
|
- backend-node/src/controllers/
|
||||||
|
- backend-node/src/services/
|
||||||
|
- backend-node/src/routes/
|
||||||
|
- backend-node/src/middleware/
|
||||||
|
|
||||||
|
# Code Rules
|
||||||
|
1. TypeScript strict mode
|
||||||
|
2. Error handling with try/catch
|
||||||
|
3. Comments in Korean
|
||||||
|
4. Follow existing code patterns
|
||||||
|
5. Use logger for important operations
|
||||||
|
6. Parameter binding ($1, $2) for SQL injection prevention
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# WACE ERP 파이프라인 공통 룰 (모든 에이전트 필수 준수)
|
||||||
|
|
||||||
|
## 1. 화면 유형 구분 (절대 규칙!)
|
||||||
|
|
||||||
|
이 시스템은 **관리자 메뉴**와 **사용자 메뉴**가 완전히 다른 방식으로 동작한다.
|
||||||
|
기능 구현 시 반드시 어느 유형인지 먼저 판단하라.
|
||||||
|
|
||||||
|
### 관리자 메뉴 (Admin)
|
||||||
|
- **구현 방식**: React 코드 기반 페이지 (`.tsx` 파일)
|
||||||
|
- **경로**: `frontend/app/(main)/admin/{기능명}/page.tsx`
|
||||||
|
- **메뉴 등록**: `menu_info` 테이블에 INSERT 필수 (코드만 만들고 메뉴 등록 안 하면 미완성!)
|
||||||
|
- **대상**: 시스템 설정, 사용자 관리, 결재 관리, 코드 관리 등
|
||||||
|
- **특징**: 하드코딩된 UI, 관리자만 접근
|
||||||
|
|
||||||
|
### 사용자 메뉴 (User/Screen)
|
||||||
|
- **구현 방식**: 로우코드 기반 (DB에 JSON으로 화면 구성 저장)
|
||||||
|
- **데이터 저장**: `screen_layouts` 테이블에 JSON 형식 보관
|
||||||
|
- **화면 디자이너**: 스크린 디자이너로 드래그앤드롭 구성
|
||||||
|
- **V2 컴포넌트**: `frontend/lib/registry/components/v2-*` 디렉토리
|
||||||
|
- **대상**: 일반 업무 화면, BOM, 문서 관리 등
|
||||||
|
- **특징**: 코드 수정 없이 화면 구성 변경 가능
|
||||||
|
|
||||||
|
### 판단 기준
|
||||||
|
|
||||||
|
| 질문 | 관리자 메뉴 | 사용자 메뉴 |
|
||||||
|
|------|-------------|-------------|
|
||||||
|
| 누가 쓰나? | 시스템 관리자 | 일반 사용자 |
|
||||||
|
| 화면 구조 고정? | 고정 (코드) | 유동적 (JSON) |
|
||||||
|
| URL 패턴 | `/admin/*` | 스크린 디자이너 경유 |
|
||||||
|
| 메뉴 등록 | `menu_info` INSERT 필수 | 스크린 레이아웃 등록 |
|
||||||
|
|
||||||
|
## 2. 관리자 메뉴 등록 (코드 구현 후 필수!)
|
||||||
|
|
||||||
|
관리자 기능을 코드로 만들었으면 반드시 `menu_info`에 등록해야 한다.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 예시: 결재 템플릿 관리 메뉴 등록
|
||||||
|
INSERT INTO menu_info (menu_id, menu_name, url, parent_id, menu_type, sort_order, is_active, company_code)
|
||||||
|
VALUES ('approvalTemplate', '결재 템플릿', '/admin/approvalTemplate', 'approval', 'ADMIN', 40, 'Y', '대상회사코드');
|
||||||
|
```
|
||||||
|
|
||||||
|
- 기존 메뉴 구조를 먼저 조회해서 parent_id, sort_order 등을 맞춰라
|
||||||
|
- company_code 별로 등록이 필요할 수 있다
|
||||||
|
- menu_auth_group 권한 매핑도 필요하면 추가
|
||||||
|
|
||||||
|
## 3. 하드코딩 금지 / 범용성 필수
|
||||||
|
|
||||||
|
- 특정 회사에만 동작하는 코드 금지
|
||||||
|
- 특정 사용자 ID에 의존하는 로직 금지
|
||||||
|
- 매직 넘버 사용 금지 (상수 또는 설정 파일로 관리)
|
||||||
|
- 하드코딩 색상 금지 (CSS 변수 사용: bg-primary, text-destructive 등)
|
||||||
|
- 하드코딩 URL 금지 (환경 변수 또는 API 클라이언트 사용)
|
||||||
|
|
||||||
|
## 4. 테스트 환경 정보
|
||||||
|
|
||||||
|
- **테스트 계정**: userId=`wace`, password=`qlalfqjsgh11`
|
||||||
|
- **역할**: SUPER_ADMIN (company_code = "*")
|
||||||
|
- **개발 프론트엔드**: http://localhost:9771
|
||||||
|
- **개발 백엔드 API**: http://localhost:8080
|
||||||
|
- **개발 DB**: postgresql://postgres:ph0909!!@39.117.244.52:11132/plm
|
||||||
|
|
||||||
|
## 5. 기능 구현 완성 체크리스트
|
||||||
|
|
||||||
|
기능 하나를 "완성"이라고 말하려면 아래를 전부 충족해야 한다:
|
||||||
|
|
||||||
|
- [ ] DB: 마이그레이션 작성 + 실행 완료
|
||||||
|
- [ ] DB: company_code 컬럼 + 인덱스 존재
|
||||||
|
- [ ] BE: API 엔드포인트 구현 + 라우트 등록
|
||||||
|
- [ ] BE: company_code 필터링 적용
|
||||||
|
- [ ] FE: API 클라이언트 함수 작성 (lib/api/)
|
||||||
|
- [ ] FE: 화면 컴포넌트 구현
|
||||||
|
- [ ] **메뉴 등록**: 관리자 메뉴면 menu_info INSERT, 사용자 메뉴면 스크린 레이아웃 등록
|
||||||
|
- [ ] 빌드 통과: 백엔드 tsc + 프론트엔드 tsc
|
||||||
|
|
||||||
|
## 6. 절대 하지 말 것
|
||||||
|
|
||||||
|
1. 페이지 파일만 만들고 메뉴 등록 안 하기 (미완성!)
|
||||||
|
2. fetch() 직접 사용 (lib/api/ 클라이언트 필수)
|
||||||
|
3. company_code 필터링 빠뜨리기
|
||||||
|
4. 하드코딩 색상/URL/사용자ID 사용
|
||||||
|
5. Card 안에 Card 중첩 (중첩 박스 금지)
|
||||||
|
6. 백엔드 재실행하기 (nodemon이 자동 재시작)
|
||||||
|
|
||||||
|
## 7. 파이프라인 태스크 설계 원칙
|
||||||
|
|
||||||
|
### 파일 스코프 규칙
|
||||||
|
- task의 `files`에 명시되지 않은 파일을 수정하면 라운드 종료 시 자동 롤백됨
|
||||||
|
- 크로스커팅 수정(SSR 호환, import 경로 변경 등)은 반드시 **별도 task**로 분리해야 함
|
||||||
|
- 한 task에 파일이 너무 많으면(3개+) 에이전트가 tool_call 처리 중 멈출 수 있음
|
||||||
|
|
||||||
|
### 대형 파일 규칙
|
||||||
|
- 1000줄 이상 파일: task당 **최대 2개**
|
||||||
|
- 2000줄 이상 파일: task당 **1개만**
|
||||||
|
- 파일 크기를 모르면 `wc -l`로 먼저 확인 후 task 분할
|
||||||
|
- 3개 대형 파일(1000줄+)을 한 task에 넣으면 hang 위험 매우 높음
|
||||||
|
|
||||||
|
### 파일럿 패턴 (새 UI 패턴 일괄 적용 시)
|
||||||
|
- 새로운 디자인 패턴을 10개+ 파일에 적용할 때:
|
||||||
|
1. **task-1 (파일럿)**: 대표 1개 파일에 시범 적용
|
||||||
|
2. **PM 확인**: 사용자 피드백 수집 (파이프라인 중단 후)
|
||||||
|
3. **task-2~N (전파)**: 확인된 패턴을 나머지 파일에 적용
|
||||||
|
- 사용자 피드백 없이 일괄 적용하면 전부 롤백해야 할 위험이 있음
|
||||||
|
- 예시: max-h 스크롤 제한을 14개 파일에 일괄 적용 -> 사용자가 거부 -> 14개 파일 전부 원복 필요
|
||||||
|
|
||||||
|
### hang 프로세스 대응
|
||||||
|
- 10분 이상 출력 없으면 hang으로 판단
|
||||||
|
- `kill <pid>`로 프로세스 종료
|
||||||
|
- `git diff --stat HEAD`로 커밋된 변경 확인
|
||||||
|
- 남은 task를 새 plan.md로 재작성하여 resume
|
||||||
|
- hang이 잦으면 task당 파일 수를 줄이거나 timeout을 늘릴 것
|
||||||
|
|
||||||
|
### 파이프라인으로 적합하지 않은 작업
|
||||||
|
- Props/데이터 플로우 디버깅 (교차 컴포넌트 의존 관계 추적)
|
||||||
|
- SSR 호환성 같은 크로스커팅 수정 (files scope 제한 때문)
|
||||||
|
- 사용자 확인 없이 디자인 패턴 일괄 전파 (거부 시 전체 롤백 위험)
|
||||||
|
- 이런 작업은 PM이 직접 처리하는 것이 10배 빠르고 안전함
|
||||||
|
|
||||||
|
## 8. 프로젝트 필수 패턴 (코드 생성 시 반드시 준수)
|
||||||
|
|
||||||
|
### Backend 필수
|
||||||
|
- **라우트 등록**: 새 라우트 파일 생성 시 반드시 `backend-node/src/app.ts`에 import + `app.use()` 등록. 이거 안 하면 API 404.
|
||||||
|
- **권한 체크**: `req.user?.isAdmin` 쓰지 마라. 반드시 `import { isAdmin, isSuperAdmin } from "../utils/permissionUtils"`의 함수를 사용. 예: `if (!isAdmin(req.user))`.
|
||||||
|
- **DB 쿼리**: `import { getPool } from "../database/db"` 사용. `const pool = getPool(req)` 패턴.
|
||||||
|
- **멀티테넌시**: `company_code` 필터 필수. `companyCode === "*"`이면 전체 조회 허용 (SUPER_ADMIN).
|
||||||
|
- **에러 처리**: try/catch + `logger.error()` + `res.status(500).json({ success: false, message: "..." })`.
|
||||||
|
|
||||||
|
### Frontend 필수
|
||||||
|
- **페이지 등록**: 새 관리자 페이지 생성 시 반드시 2곳 등록:
|
||||||
|
1. `frontend/app/(main)/admin/{기능명}/page.tsx` 파일 생성 (wrapper)
|
||||||
|
2. `frontend/components/layout/AdminPageRenderer.tsx`의 `ADMIN_PAGE_REGISTRY`에 추가
|
||||||
|
이거 안 하면 페이지 접근 불가.
|
||||||
|
- **API 호출**: `import { apiClient } from "@/lib/api/client"` 사용. fetch 직접 쓰지 마라.
|
||||||
|
- **인증 훅**: `import { useAuth } from "@/hooks/useAuth"`. `const { isAdmin } = useAuth()`.
|
||||||
|
- **UI 컴포넌트**: shadcn/ui 기반 (`@/components/ui/*`). 새 UI 라이브러리 설치 금지.
|
||||||
|
- **목록 페이지**: `ResponsiveDataView` + `Pagination` 컴포넌트 사용.
|
||||||
|
- **아이콘**: `lucide-react`에서 import.
|
||||||
|
|
||||||
|
### DB 필수
|
||||||
|
- **마이그레이션 파일**: `db/migrations/YYMMDD_설명.sql` 형식.
|
||||||
|
- **멱등성**: `CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS` 사용.
|
||||||
|
- **company_code**: 모든 비즈니스 테이블에 `company_code VARCHAR(20) NOT NULL` 필수.
|
||||||
|
|
||||||
|
## 9. 설정 패널 디자인 규칙 (UI 작업 시 필수)
|
||||||
|
|
||||||
|
설정 패널(config panel) 관련 작업 시 반드시 `_local/agent-pipeline/agents/ui-design-philosophy.mdc`를 참조하라.
|
||||||
|
|
||||||
|
### 핵심 규칙 요약
|
||||||
|
- **참조 모델**: `V2SelectConfigPanel.tsx` (Gold Standard)
|
||||||
|
- **섹션 헤더**: Icon + Label + Badge 카운트
|
||||||
|
- **텍스트 오버플로우**: `min-w-0 flex-1` + `truncate` 필수
|
||||||
|
- **max-h 금지**: 인라인 콘텐츠(CollapsibleContent, div)에 max-h/overflow-y-auto 절대 금지
|
||||||
|
- **max-h 허용**: CommandGroup, CommandList 등 드롭다운 팝업만
|
||||||
|
- **Progressive Disclosure**: 고급 설정은 Collapsible 기본 닫힘 + Badge 상태 요약
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: pipeline-db
|
||||||
|
description: Agent Pipeline DB 전문가. PostgreSQL 스키마 설계, 마이그레이션 작성 및 실행. 모든 테이블에 company_code 필수.
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role
|
||||||
|
You are a Database specialist for ERP-node project.
|
||||||
|
Stack: PostgreSQL + Raw Query (no ORM). Migrations in db/migrations/.
|
||||||
|
|
||||||
|
# CRITICAL PROJECT RULES
|
||||||
|
|
||||||
|
## 1. Multi-tenancy (ABSOLUTE MUST!)
|
||||||
|
- ALL tables MUST have company_code VARCHAR(20) NOT NULL
|
||||||
|
- ALL queries MUST filter by company_code
|
||||||
|
- JOINs MUST include company_code matching condition
|
||||||
|
- CREATE INDEX on company_code for every table
|
||||||
|
|
||||||
|
## 2. Migration Rules
|
||||||
|
- File naming: NNN_description.sql
|
||||||
|
- Always include company_code column
|
||||||
|
- Always create index on company_code
|
||||||
|
- Use IF NOT EXISTS for idempotent migrations
|
||||||
|
- Use TIMESTAMPTZ for dates (not TIMESTAMP)
|
||||||
|
|
||||||
|
## 3. MIGRATION EXECUTION (절대 규칙!)
|
||||||
|
마이그레이션 SQL 파일을 생성한 후, 반드시 직접 실행해서 테이블을 생성해라.
|
||||||
|
절대 사용자에게 "직접 실행해주세요"라고 떠넘기지 마라.
|
||||||
|
|
||||||
|
Docker 환경:
|
||||||
|
```bash
|
||||||
|
DOCKER_HOST=unix:///Users/gbpark/.orbstack/run/docker.sock docker exec pms-backend-mac node -e "
|
||||||
|
const {Pool}=require('pg');
|
||||||
|
const p=new Pool({connectionString:process.env.DATABASE_URL,ssl:false});
|
||||||
|
const fs=require('fs');
|
||||||
|
const sql=fs.readFileSync('/app/db/migrations/파일명.sql','utf8');
|
||||||
|
p.query(sql).then(()=>{console.log('OK');p.end()}).catch(e=>{console.error(e.message);p.end();process.exit(1)})
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
# Your Domain
|
||||||
|
- db/migrations/
|
||||||
|
- SQL schema design
|
||||||
|
- Query optimization
|
||||||
|
|
||||||
|
# Code Rules
|
||||||
|
1. PostgreSQL syntax only
|
||||||
|
2. Parameter binding ($1, $2)
|
||||||
|
3. Use COALESCE for NULL handling
|
||||||
|
4. Use TIMESTAMPTZ for dates
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: pipeline-frontend
|
||||||
|
description: Agent Pipeline 프론트엔드 전문가. Next.js 14 + React + TypeScript + shadcn/ui 기반 화면 구현. fetch 직접 사용 금지, lib/api/ 클라이언트 필수.
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role
|
||||||
|
You are a Frontend specialist for ERP-node project.
|
||||||
|
Stack: Next.js 14 + React + TypeScript + Tailwind CSS + shadcn/ui.
|
||||||
|
|
||||||
|
# CRITICAL PROJECT RULES
|
||||||
|
|
||||||
|
## 1. API Client (ABSOLUTE RULE!)
|
||||||
|
- NEVER use fetch() directly!
|
||||||
|
- ALWAYS use lib/api/ clients (Axios-based)
|
||||||
|
- 환경별 URL 자동 처리: v1.vexplor.com → api.vexplor.com, localhost → localhost:8080
|
||||||
|
|
||||||
|
## 2. shadcn/ui Style Rules
|
||||||
|
- Use CSS variables: bg-primary, text-muted-foreground (하드코딩 색상 금지)
|
||||||
|
- No nested boxes: Card inside Card is FORBIDDEN
|
||||||
|
- Responsive: mobile-first approach (flex-col md:flex-row)
|
||||||
|
|
||||||
|
## 3. V2 Component Standard
|
||||||
|
V2 컴포넌트를 만들거나 수정할 때 반드시 이 규격을 따라야 한다.
|
||||||
|
|
||||||
|
### 폴더 구조 (필수)
|
||||||
|
```
|
||||||
|
frontend/lib/registry/components/v2-{name}/
|
||||||
|
├── index.ts # createComponentDefinition() 호출
|
||||||
|
├── types.ts # Config extends ComponentConfig
|
||||||
|
├── {Name}Component.tsx # React 함수 컴포넌트
|
||||||
|
├── {Name}Renderer.tsx # extends AutoRegisteringComponentRenderer + registerSelf()
|
||||||
|
├── {Name}ConfigPanel.tsx # ConfigPanelBuilder 사용
|
||||||
|
└── config.ts # 기본 설정값 상수
|
||||||
|
```
|
||||||
|
|
||||||
|
### ConfigPanel 규칙 (절대!)
|
||||||
|
- 반드시 ConfigPanelBuilder 또는 ConfigSection 사용
|
||||||
|
- 직접 JSX로 설정 UI 작성 금지
|
||||||
|
|
||||||
|
## 4. API Client 생성 패턴
|
||||||
|
```typescript
|
||||||
|
// frontend/lib/api/yourModule.ts
|
||||||
|
import apiClient from "@/lib/api/client";
|
||||||
|
|
||||||
|
export async function getYourData(id: number) {
|
||||||
|
const response = await apiClient.get(`/api/your-endpoint/${id}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Your Domain
|
||||||
|
- frontend/components/
|
||||||
|
- frontend/app/
|
||||||
|
- frontend/lib/
|
||||||
|
- frontend/hooks/
|
||||||
|
|
||||||
|
# Code Rules
|
||||||
|
1. TypeScript strict mode
|
||||||
|
2. React functional components with hooks
|
||||||
|
3. Prefer shadcn/ui components
|
||||||
|
4. Use cn() utility for conditional classes
|
||||||
|
5. Comments in Korean
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
name: pipeline-ui
|
||||||
|
description: Agent Pipeline UI/UX 디자인 전문가. 모던 엔터프라이즈 UI 구현. CSS 변수 필수, 하드코딩 색상 금지, 반응형 필수.
|
||||||
|
model: inherit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role
|
||||||
|
You are a UI/UX Design specialist for the ERP-node project.
|
||||||
|
Stack: Next.js 14 + React + TypeScript + Tailwind CSS + shadcn/ui + lucide-react icons.
|
||||||
|
|
||||||
|
# Design Philosophy: Palantir + Toss
|
||||||
|
- **토스**: 쉬운 게 맞다. 한 화면에 하나의 질문. 몰라도 되는 건 숨기기. ~해요 체 사용.
|
||||||
|
- **팔란티어**: Dense but organized. 시각적 계층으로 정보 정리. 뷰당 최대 10개 항목.
|
||||||
|
- Dark mode compatible using CSS variables
|
||||||
|
- Subtle animations and micro-interactions
|
||||||
|
|
||||||
|
# CRITICAL STYLE RULES
|
||||||
|
|
||||||
|
## 1. Color System (CSS Variables ONLY)
|
||||||
|
- bg-background / text-foreground (base)
|
||||||
|
- bg-primary / text-primary-foreground (actions)
|
||||||
|
- bg-muted / text-muted-foreground (secondary)
|
||||||
|
- bg-destructive / text-destructive-foreground (danger)
|
||||||
|
- 선택/활성: border-primary bg-primary/5 ring-1 ring-primary/20
|
||||||
|
- 비활성 배경: bg-muted/30
|
||||||
|
FORBIDDEN: bg-gray-50, text-blue-500, bg-white, text-black
|
||||||
|
|
||||||
|
## 2. Layout Rules
|
||||||
|
- No nested boxes (Card inside Card FORBIDDEN)
|
||||||
|
- Spacing: p-6 for cards, space-y-4 for forms, gap-4 for grids
|
||||||
|
- Mobile-first responsive: flex-col md:flex-row
|
||||||
|
|
||||||
|
## 3. Typography
|
||||||
|
- Page title: text-3xl font-bold
|
||||||
|
- Section: text-xl font-semibold
|
||||||
|
- Body: text-sm
|
||||||
|
- Helper: text-xs text-muted-foreground
|
||||||
|
- Config panel description: text-[10px] or text-[11px] text-muted-foreground
|
||||||
|
|
||||||
|
## 4. Components
|
||||||
|
- ALWAYS use shadcn/ui components
|
||||||
|
- Use cn() for conditional classes
|
||||||
|
- Use lucide-react for ALL icons
|
||||||
|
|
||||||
|
## 5. Config Panel Design Patterns (필수!)
|
||||||
|
|
||||||
|
설정 패널 작업 시 반드시 아래 패턴을 따라라.
|
||||||
|
**참조 파일 (Gold Standard)**: `V2SelectConfigPanel.tsx`
|
||||||
|
|
||||||
|
### 섹션 헤더: Icon + Label + Badge
|
||||||
|
```tsx
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon className="h-4 w-4 text-primary" />
|
||||||
|
<p className="text-sm font-medium">섹션 제목</p>
|
||||||
|
<Badge variant="secondary" className="ml-auto text-[10px] px-1.5 py-0">{count}</Badge>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 상태 표시 카드: 설정됨/안됨 시각화
|
||||||
|
- 활성: `border-primary/30 bg-primary/5` + CheckCircle2
|
||||||
|
- 비활성: `bg-muted/30` + Circle (muted-foreground/40)
|
||||||
|
- 텍스트: `min-w-0 flex-1` + `truncate` 필수
|
||||||
|
|
||||||
|
### Switch + 설명: 토글 옵션
|
||||||
|
- 텍스트 블록: `min-w-0 flex-1 mr-3`
|
||||||
|
- 제목: text-xs font-medium, 설명: text-[10px] text-muted-foreground
|
||||||
|
|
||||||
|
### 숫자 입력 그리드: 2~3개
|
||||||
|
- `grid grid-cols-2` 또는 `grid-cols-3 gap-2`
|
||||||
|
- 각 셀: rounded-lg border bg-muted/30 p-3 text-center
|
||||||
|
|
||||||
|
### Collapsible (Progressive Disclosure)
|
||||||
|
- 고급/드문 설정은 Collapsible로 접기 (기본 닫힘)
|
||||||
|
- 접혀있을 때 Badge로 상태 요약
|
||||||
|
- **펼친 콘텐츠에 max-h / overflow-y-auto 절대 금지!**
|
||||||
|
|
||||||
|
## 6. Scroll Restriction Policy (절대 규칙!)
|
||||||
|
- 인라인 콘텐츠 (CollapsibleContent, div 안 리스트)에 max-h 금지
|
||||||
|
- max-h 허용 대상: CommandGroup, CommandList (드롭다운 팝업만)
|
||||||
|
- 펼치기 = 전부 보여주기. 스크롤 제한 = 눈가리기.
|
||||||
|
|
||||||
|
## 7. Text Overflow (필수)
|
||||||
|
- 모든 동적 텍스트: truncate + 부모에 min-w-0
|
||||||
|
- Switch/Select 옆 블록: min-w-0 flex-1 mr-3
|
||||||
|
- 고정 폭 컨트롤: shrink-0
|
||||||
|
|
||||||
|
# Your Domain
|
||||||
|
- frontend/components/ (UI components)
|
||||||
|
- frontend/app/ (pages)
|
||||||
|
|
||||||
|
# Output Rules
|
||||||
|
1. TypeScript strict mode
|
||||||
|
2. "use client" for client components
|
||||||
|
3. Comments in Korean
|
||||||
|
4. MINIMAL targeted changes when modifying existing files
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
name: pipeline-verifier
|
||||||
|
description: Agent Pipeline 검증 전문가. 구현 완료 후 실제 동작 검증. 빈 껍데기 탐지, 패턴 준수 확인, 멀티테넌시 검증.
|
||||||
|
model: fast
|
||||||
|
readonly: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Role
|
||||||
|
You are a skeptical validator for the ERP-node project.
|
||||||
|
Your job is to verify that work claimed as complete actually works.
|
||||||
|
|
||||||
|
# Verification Checklist
|
||||||
|
|
||||||
|
## 1. Multi-tenancy (최우선)
|
||||||
|
- [ ] 모든 SQL에 company_code 필터 존재
|
||||||
|
- [ ] req.user!.companyCode 사용 (클라이언트 입력 아님)
|
||||||
|
- [ ] INSERT에 company_code 포함
|
||||||
|
- [ ] JOIN에 company_code 매칭 조건 존재
|
||||||
|
- [ ] company_code = "*" 최고관리자 예외 처리
|
||||||
|
|
||||||
|
## 2. Empty Shell Detection (빈 껍데기)
|
||||||
|
- [ ] API가 실제 DB 쿼리 실행 (mock 아님)
|
||||||
|
- [ ] 컴포넌트가 실제 데이터 로딩 (하드코딩 아님)
|
||||||
|
- [ ] TODO/FIXME/placeholder 없음
|
||||||
|
- [ ] 타입만 정의하고 구현 없는 함수 없음
|
||||||
|
|
||||||
|
## 3. Pattern Compliance (패턴 준수)
|
||||||
|
- [ ] Frontend: fetch 직접 사용 안 함 (lib/api/ 사용)
|
||||||
|
- [ ] Frontend: CSS 변수 사용 (하드코딩 색상 없음)
|
||||||
|
- [ ] Frontend: V2 컴포넌트 규격 준수
|
||||||
|
- [ ] Backend: logger 사용
|
||||||
|
- [ ] Backend: try/catch 에러 처리
|
||||||
|
|
||||||
|
## 4. Integration Check
|
||||||
|
- [ ] Route가 index.ts에 등록됨
|
||||||
|
- [ ] Import 경로 정확
|
||||||
|
- [ ] Export 존재
|
||||||
|
- [ ] TypeScript 타입 일치
|
||||||
|
|
||||||
|
# Reporting Format
|
||||||
|
```
|
||||||
|
## 검증 결과: [PASS/FAIL]
|
||||||
|
|
||||||
|
### 통과 항목
|
||||||
|
- item 1
|
||||||
|
- item 2
|
||||||
|
|
||||||
|
### 실패 항목
|
||||||
|
- item 1: 구체적 이유
|
||||||
|
- item 2: 구체적 이유
|
||||||
|
|
||||||
|
### 권장 수정사항
|
||||||
|
- fix 1
|
||||||
|
- fix 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not accept claims at face value. Check the actual code.
|
||||||
@@ -0,0 +1,768 @@
|
|||||||
|
/* ============================================================
|
||||||
|
ERP Preset Common CSS
|
||||||
|
- erp-node globals.css 기준 디자인 토큰
|
||||||
|
- Vivid Blue 테마 (NOT Indigo)
|
||||||
|
- Palantir 밀도 + Toss 친화
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* ===== Google Fonts ===== */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
||||||
|
|
||||||
|
/* ===== Design Tokens (globals.css 동기화) ===== */
|
||||||
|
:root {
|
||||||
|
/* Light Theme (기본) */
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 224 71% 4%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 224 71% 4%;
|
||||||
|
--primary: 217.2 91.2% 59.8%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 220 14.3% 95.9%;
|
||||||
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
--muted: 220 14.3% 95.9%;
|
||||||
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
|
--accent: 220 14.3% 95.9%;
|
||||||
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--border: 220 13% 88%;
|
||||||
|
--input: 220 13% 88%;
|
||||||
|
--ring: 217.2 91.2% 59.8%;
|
||||||
|
--success: 142 76% 36%;
|
||||||
|
--warning: 38 92% 50%;
|
||||||
|
--info: 188 94% 43%;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
--radius: 8px;
|
||||||
|
--radius-sm: 4px;
|
||||||
|
--radius-md: 6px;
|
||||||
|
--radius-lg: 8px;
|
||||||
|
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Theme (Palantir-Inspired Deep Navy) */
|
||||||
|
.dark {
|
||||||
|
--background: 222 47% 6%;
|
||||||
|
--foreground: 210 20% 95%;
|
||||||
|
--card: 220 40% 9%;
|
||||||
|
--card-foreground: 210 20% 95%;
|
||||||
|
--primary: 217 91% 65%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 220 25% 14%;
|
||||||
|
--secondary-foreground: 210 20% 90%;
|
||||||
|
--muted: 220 20% 13%;
|
||||||
|
--muted-foreground: 215 15% 58%;
|
||||||
|
--accent: 220 25% 16%;
|
||||||
|
--accent-foreground: 210 20% 90%;
|
||||||
|
--destructive: 0 72% 51%;
|
||||||
|
--border: 220 20% 18%;
|
||||||
|
--input: 220 20% 18%;
|
||||||
|
--ring: 217 91% 65%;
|
||||||
|
--success: 142 70% 42%;
|
||||||
|
--warning: 38 92% 55%;
|
||||||
|
--info: 188 90% 48%;
|
||||||
|
--chart-1: 220 70% 55%;
|
||||||
|
--chart-2: 160 60% 48%;
|
||||||
|
--chart-3: 30 80% 58%;
|
||||||
|
--chart-4: 280 65% 63%;
|
||||||
|
--chart-5: 340 75% 58%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Reset & Base ===== */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: hsl(var(--background));
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 인터랙티브 요소 트랜지션 ===== */
|
||||||
|
button, a, input, textarea, select {
|
||||||
|
transition: color 150ms ease, background-color 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 스크롤바 ===== */
|
||||||
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||||
|
::-webkit-scrollbar-track { background: hsl(var(--muted)); }
|
||||||
|
::-webkit-scrollbar-thumb { background: hsl(var(--muted-foreground) / 0.3); border-radius: 5px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground) / 0.5); }
|
||||||
|
* { scrollbar-width: thin; scrollbar-color: hsl(var(--muted-foreground) / 0.3) hsl(var(--muted)); }
|
||||||
|
|
||||||
|
/* ===== Layout ===== */
|
||||||
|
.page-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 검색 필터 ===== */
|
||||||
|
.search-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter .field-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 입력 필드 ===== */
|
||||||
|
input[type="text"], input[type="number"], input[type="date"],
|
||||||
|
input[type="search"], select, textarea {
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: hsl(var(--background));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus, textarea:focus {
|
||||||
|
border-color: hsl(var(--ring));
|
||||||
|
box-shadow: 0 0 0 3px hsl(var(--ring) / 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder { color: hsl(var(--muted-foreground) / 0.6); }
|
||||||
|
|
||||||
|
textarea { height: auto; padding: 8px 10px; min-height: 60px; resize: vertical; }
|
||||||
|
|
||||||
|
/* ===== 버튼 ===== */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm { height: 30px; padding: 0 10px; font-size: 12px; }
|
||||||
|
.btn-xs { height: 26px; padding: 0 8px; font-size: 11px; }
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: hsl(var(--primary));
|
||||||
|
color: hsl(var(--primary-foreground));
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
box-shadow: 0 2px 12px hsl(var(--primary) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-destructive {
|
||||||
|
background: hsl(var(--destructive) / 0.08);
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
border-color: hsl(var(--destructive) / 0.2);
|
||||||
|
}
|
||||||
|
.btn-destructive:hover {
|
||||||
|
background: hsl(var(--destructive) / 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.btn-ghost:hover { background: hsl(var(--muted)); color: hsl(var(--foreground)); }
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn svg { width: 16px; height: 16px; }
|
||||||
|
.btn-sm svg { width: 14px; height: 14px; }
|
||||||
|
|
||||||
|
/* ===== 카드 / 패널 ===== */
|
||||||
|
.card {
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 뱃지 ===== */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-count {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary { background: hsl(var(--primary) / 0.1); color: hsl(var(--primary)); }
|
||||||
|
.badge-success { background: hsl(var(--success) / 0.1); color: hsl(var(--success)); }
|
||||||
|
.badge-warning { background: hsl(var(--warning) / 0.1); color: hsl(var(--warning)); }
|
||||||
|
.badge-danger { background: hsl(var(--destructive) / 0.1); color: hsl(var(--destructive)); }
|
||||||
|
.badge-info { background: hsl(var(--info) / 0.1); color: hsl(var(--info)); }
|
||||||
|
.badge-muted { background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
|
||||||
|
|
||||||
|
/* ===== 데이터 테이블 ===== */
|
||||||
|
.data-table-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover {
|
||||||
|
background: hsl(var(--muted) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr.selected {
|
||||||
|
background: hsl(var(--primary) / 0.06);
|
||||||
|
border-left: 3px solid hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr.selected td:first-child {
|
||||||
|
padding-left: 9px; /* 3px border 보상 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 숫자 / 코드 셀 */
|
||||||
|
.cell-mono {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-number {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 체크박스 */
|
||||||
|
.data-table input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
accent-color: hsl(var(--primary));
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 리사이즈 핸들 (좌우 분할) ===== */
|
||||||
|
.resize-handle {
|
||||||
|
width: 6px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: hsl(var(--border));
|
||||||
|
transition: background 150ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover,
|
||||||
|
.resize-handle.active {
|
||||||
|
background: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 2px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: hsl(var(--muted-foreground) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 탭 ===== */
|
||||||
|
.tabs-header {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: color 150ms, border-color 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn .tab-badge {
|
||||||
|
background: hsl(var(--primary) / 0.1);
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
|
||||||
|
/* ===== 모달 / 다이얼로그 ===== */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 1040;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transform: translateY(8px);
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.open .modal-content {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 다크모드 모달 그림자 강화 */
|
||||||
|
.dark .modal-content {
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-sm { width: min(480px, 92vw); }
|
||||||
|
.modal-md { width: min(640px, 92vw); }
|
||||||
|
.modal-lg { width: min(900px, 95vw); }
|
||||||
|
.modal-xl { width: min(1200px, 95vw); }
|
||||||
|
.modal-full { width: 95vw; height: 90vh; }
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 닫기 버튼 */
|
||||||
|
.modal-close {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.modal-close:hover {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 확인 다이얼로그 ===== */
|
||||||
|
.confirm-dialog .modal-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-icon.warn {
|
||||||
|
background: hsl(var(--warning) / 0.1);
|
||||||
|
color: hsl(var(--warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-icon.danger {
|
||||||
|
background: hsl(var(--destructive) / 0.1);
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 빈 상태 ===== */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
border: 2px dashed hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state svg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
color: hsl(var(--muted-foreground) / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .empty-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: hsl(var(--muted-foreground) / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 폼 그리드 ===== */
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid-2 { grid-template-columns: 1fr 1fr; }
|
||||||
|
.form-grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||||
|
.form-grid-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label .required {
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 페이지네이션 ===== */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
font-size: 12px;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info { font-family: var(--font-mono); }
|
||||||
|
|
||||||
|
.pagination-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
background: none;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.page-btn:hover { background: hsl(var(--muted)); color: hsl(var(--foreground)); }
|
||||||
|
.page-btn.active { background: hsl(var(--primary)); color: white; border-color: hsl(var(--primary)); }
|
||||||
|
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* ===== 상태 뱃지 (한글) ===== */
|
||||||
|
.status-확정, .status-사용, .status-정상, .status-완료 { background: hsl(var(--success) / 0.1); color: hsl(var(--success)); }
|
||||||
|
.status-진행중, .status-진행, .status-점검중 { background: hsl(var(--primary) / 0.1); color: hsl(var(--primary)); }
|
||||||
|
.status-대기, .status-교정예정 { background: hsl(var(--warning) / 0.1); color: hsl(var(--warning)); }
|
||||||
|
.status-취소, .status-미사용, .status-폐기, .status-수리중 { background: hsl(var(--destructive) / 0.1); color: hsl(var(--destructive)); }
|
||||||
|
|
||||||
|
/* ===== 유틸리티 ===== */
|
||||||
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.flex-1 { flex: 1; }
|
||||||
|
.gap-2 { gap: 8px; }
|
||||||
|
.gap-3 { gap: 12px; }
|
||||||
|
.gap-4 { gap: 16px; }
|
||||||
|
.mt-2 { margin-top: 8px; }
|
||||||
|
.mt-3 { margin-top: 12px; }
|
||||||
|
.mb-2 { margin-bottom: 8px; }
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* ===== 테마 토글 ===== */
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 9999;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
background: hsl(var(--card));
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 프로그레스 바 ===== */
|
||||||
|
.progress-bar {
|
||||||
|
height: 4px;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 300ms ease;
|
||||||
|
}
|
||||||
|
.progress-green { background: hsl(var(--success)); }
|
||||||
|
.progress-yellow { background: hsl(var(--warning)); }
|
||||||
|
.progress-red { background: hsl(var(--destructive)); }
|
||||||
|
|
||||||
|
/* ===== 트리뷰 ===== */
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 100ms;
|
||||||
|
}
|
||||||
|
.tree-node:hover { background: hsl(var(--muted) / 0.5); }
|
||||||
|
.tree-node.selected {
|
||||||
|
background: hsl(var(--primary) / 0.06);
|
||||||
|
border-left: 3px solid hsl(var(--primary));
|
||||||
|
}
|
||||||
|
.tree-toggle {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
transition: transform 150ms;
|
||||||
|
}
|
||||||
|
.tree-toggle.expanded { transform: rotate(90deg); }
|
||||||
|
.tree-type-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 애니메이션 ===== */
|
||||||
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
|
@keyframes fadeInUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes slideDown { from { opacity: 0; max-height: 0; } to { opacity: 1; max-height: 500px; } }
|
||||||
|
|
||||||
|
.animate-fadeIn { animation: fadeIn 200ms ease; }
|
||||||
|
.animate-fadeInUp { animation: fadeInUp 200ms ease; }
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,411 @@
|
|||||||
|
# UI/UX 디자인 절대 철학 (Palantir + Toss)
|
||||||
|
|
||||||
|
## 핵심 철학
|
||||||
|
|
||||||
|
이 프로젝트의 UI는 **팔란티어의 정보 밀도**와 **토스의 사용자 중심 철학**을 결합한다.
|
||||||
|
|
||||||
|
### 토스 철학 (사용성)
|
||||||
|
- **쉬운 게 맞다**: 사용자가 고민하지 않아도 되게 만들어라
|
||||||
|
- **한 화면에 하나의 질문**: 설정을 나열하지 말고 단계별로 안내해라
|
||||||
|
- **선택지 최소화**: 10개 옵션 대신 가장 많이 쓰는 2-3개만 보여주고 나머지는 숨겨라
|
||||||
|
- **몰라도 되는 건 숨기기**: 고급 설정은 기본적으로 접혀있어야 한다
|
||||||
|
- **말하듯이 설명**: 전문 용어 대신 자연스러운 한국어로 안내해라 ("Z-Index" -> "앞/뒤 순서")
|
||||||
|
- **기본값이 최선**: 대부분의 사용자가 설정을 안 바꿔도 잘 동작해야 한다
|
||||||
|
|
||||||
|
### 팔란티어 철학 (정보 밀도)
|
||||||
|
- **Dense but organized**: 정보를 빽빽하게 넣되, 시각적 계층으로 정리해라
|
||||||
|
- **F-shaped hierarchy**: 왼쪽에서 오른쪽으로 읽는 자연스러운 흐름
|
||||||
|
- **Composition**: 작은 원자적 조각을 조합하여 복잡한 UI를 구성해라
|
||||||
|
- **뷰당 최대 10개 항목**: 한 섹션에 10개 이상 보이지 않게 분리해라
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 절대 금지 사항 (위반 시 즉시 수정)
|
||||||
|
|
||||||
|
### 1. 텍스트/요소 밀림 금지
|
||||||
|
카드, 버튼, 라벨 등에서 텍스트가 의도한 위치에서 밀리거나 어긋나면 안 된다.
|
||||||
|
- 카드 그리드에서 텍스트가 세로로 정렬되지 않는 경우 -> flex + 고정폭 또는 text-center로 해결
|
||||||
|
- 아이콘과 텍스트가 같은 줄에 있어야 하는데 밀리는 경우 -> items-center + gap으로 해결
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 금지: 텍스트가 밀리는 카드
|
||||||
|
<button className="flex flex-col items-start p-3">
|
||||||
|
<Icon /><span>직접 입력</span> // 아이콘과 텍스트가 밀림
|
||||||
|
</button>
|
||||||
|
|
||||||
|
// 필수: 정렬된 카드
|
||||||
|
<button className="flex flex-col items-center justify-center p-3 text-center min-h-[80px]">
|
||||||
|
<Icon className="h-5 w-5 mb-1.5" />
|
||||||
|
<span className="text-xs font-medium leading-tight">직접 입력</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground leading-tight mt-0.5">옵션을 직접 추가해요</span>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 입력 폭 불일치 금지
|
||||||
|
같은 영역에 있는 Input, Select 등 폼 컨트롤은 반드시 동일한 폭을 가져야 한다.
|
||||||
|
- 같은 섹션의 Input이 서로 다른 너비를 가지면 안 된다
|
||||||
|
- 나란히 배치된 필드(너비/높이 등)는 정확히 같은 폭이어야 한다
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 금지: 폭이 다른 입력 필드
|
||||||
|
<Input className="w-[120px]" /> // Z-Index
|
||||||
|
<Input className="w-[180px]" /> // 높이 <- 폭이 다름!
|
||||||
|
|
||||||
|
// 필수: 폭 일관성
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1"><Label>너비</Label><Input className="h-7 w-full" /></div>
|
||||||
|
<div className="flex-1"><Label>높이</Label><Input className="h-7 w-full" /></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Z-Index</Label>
|
||||||
|
<Input className="h-7 w-full" /> // 같은 w-full
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 미작동 옵션 표시 금지
|
||||||
|
실제로 동작하지 않는 설정 옵션을 사용자에게 보여주면 안 된다.
|
||||||
|
- 기능이 구현되지 않은 옵션은 숨기거나 "준비 중" 표시
|
||||||
|
- 특정 조건에서만 동작하는 옵션은 해당 조건이 아닐 때 비활성화(disabled) 처리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 설정 패널 디자인 패턴
|
||||||
|
|
||||||
|
### 참조 모델 (Gold Standard)
|
||||||
|
`V2SelectConfigPanel.tsx`가 모든 설정 패널의 기준이다.
|
||||||
|
새 패널을 만들거나 기존 패널을 수정할 때 이 파일의 구조를 따라라.
|
||||||
|
|
||||||
|
```
|
||||||
|
구조: 카드 선택 (1단계) -> 소스별 상세 (2단계) -> 고급 설정 (Collapsible)
|
||||||
|
간격: space-y-4
|
||||||
|
카드: grid grid-cols-2 gap-2, rounded-lg border p-3
|
||||||
|
선택됨: border-primary bg-primary/5 ring-1 ring-primary/20
|
||||||
|
Collapsible: 접었을 때 Badge로 상태 요약
|
||||||
|
설명: text-[10px] text-muted-foreground
|
||||||
|
```
|
||||||
|
|
||||||
|
### 섹션 헤더 패턴 (Icon + Label + Badge)
|
||||||
|
모든 설정 섹션의 헤더는 아이콘, 제목, Badge 카운트를 포함한다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4 text-primary" />
|
||||||
|
<p className="text-sm font-medium">필드 매핑</p>
|
||||||
|
<Badge variant="secondary" className="ml-auto text-[10px] px-1.5 py-0">
|
||||||
|
{mappedCount}/{totalCount}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground pl-6">
|
||||||
|
상위 폼의 필드 중 렉 생성에 사용할 필드를 선택해요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**규칙:**
|
||||||
|
- 아이콘: `h-4 w-4 text-primary` (lucide-react)
|
||||||
|
- 제목: `text-sm font-medium`
|
||||||
|
- Badge: `variant="secondary" text-[10px]`, `ml-auto`로 오른쪽 정렬
|
||||||
|
- 설명: `text-[11px] text-muted-foreground pl-6` (아이콘 너비만큼 들여쓰기)
|
||||||
|
|
||||||
|
### 상태 표시 카드 리스트 패턴 (CheckCircle / Circle)
|
||||||
|
필드 매핑처럼 "설정됨/안됨"을 시각적으로 구분하는 리스트.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{items.map((item) => {
|
||||||
|
const isActive = !!item.value;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.key}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-lg border px-3 py-2 transition-colors",
|
||||||
|
isActive ? "border-primary/30 bg-primary/5" : "bg-muted/30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive
|
||||||
|
? <CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||||
|
: <Circle className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40" />}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-xs font-medium truncate">{item.label}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground truncate">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<Select className="h-7 w-[120px] shrink-0 text-xs">...</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**규칙:**
|
||||||
|
- 활성 상태: `border-primary/30 bg-primary/5` + `CheckCircle2` (primary)
|
||||||
|
- 비활성 상태: `bg-muted/30` + `Circle` (muted-foreground/40)
|
||||||
|
- 아이콘: `shrink-0`으로 줄지 않게
|
||||||
|
- 텍스트 블록: `min-w-0 flex-1`로 overflow 대비 + `truncate`
|
||||||
|
- 우측 컨트롤: `shrink-0`으로 고정 폭
|
||||||
|
|
||||||
|
### 컴팩트 숫자 입력 그리드 패턴
|
||||||
|
관련 숫자 입력(2~3개)은 가로 그리드로 배치한다. 세로 나열보다 공간 효율이 좋다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
|
||||||
|
<p className="text-[10px] text-muted-foreground">최대 조건</p>
|
||||||
|
<Input type="number" className="h-7 text-xs text-center" value={10} />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
|
||||||
|
<p className="text-[10px] text-muted-foreground">최대 열</p>
|
||||||
|
<Input type="number" className="h-7 text-xs text-center" value={99} />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
|
||||||
|
<p className="text-[10px] text-muted-foreground">최대 단</p>
|
||||||
|
<Input type="number" className="h-7 text-xs text-center" value={20} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**규칙:**
|
||||||
|
- 입력 2개: `grid-cols-2`, 입력 3개: `grid-cols-3`
|
||||||
|
- 4개 이상은 세로 나열 또는 2행 그리드 사용
|
||||||
|
- 각 셀: `rounded-lg border bg-muted/30 p-3 text-center`
|
||||||
|
- 라벨: `text-[10px] text-muted-foreground`
|
||||||
|
- Input: `text-center`로 숫자 중앙 정렬
|
||||||
|
|
||||||
|
### 카드 선택 패턴 (타입/소스 선택)
|
||||||
|
드롭다운 대신 시각적 카드로 선택하게 한다. 사용자가 뭘 선택하는지 한눈에 보인다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-3">이 필드는 어떤 데이터를 선택하나요?</p>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{cards.map(card => (
|
||||||
|
<button
|
||||||
|
key={card.value}
|
||||||
|
onClick={() => updateConfig("source", card.value)}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center rounded-lg border p-3 text-center transition-all min-h-[80px]",
|
||||||
|
isSelected
|
||||||
|
? "border-primary bg-primary/5 ring-1 ring-primary/20"
|
||||||
|
: "border-border hover:border-primary/50 hover:bg-muted/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<card.icon className="h-5 w-5 mb-1.5 text-primary" />
|
||||||
|
<span className="text-xs font-medium leading-tight">{card.title}</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground leading-tight mt-0.5">{card.description}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**카드 필수 규칙:**
|
||||||
|
- 모든 카드는 동일한 높이 (`min-h-[80px]`)
|
||||||
|
- 텍스트는 center 정렬
|
||||||
|
- 아이콘은 텍스트 위에
|
||||||
|
- 설명은 `text-[10px] text-muted-foreground`
|
||||||
|
- 선택된 카드: `border-primary bg-primary/5 ring-1 ring-primary/20`
|
||||||
|
|
||||||
|
### 고급 설정 패턴 (Progressive Disclosure)
|
||||||
|
자주 안 쓰는 설정은 Collapsible로 숨긴다. 기본은 접혀있다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Collapsible>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<button className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left hover:bg-muted/50 transition-colors">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">고급 설정</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{count > 0 && <Badge variant="secondary" className="text-[10px]">{count}개</Badge>}
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-180" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||||
|
{/* 고급 옵션들 */}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Collapsible 규칙:**
|
||||||
|
- 펼쳐진 콘텐츠에 `max-h`나 `overflow-y-auto` 절대 금지 (펼치면 전부 보여야 함)
|
||||||
|
- 접혀있을 때 Badge로 현재 상태 요약 (예: "3개 설정됨")
|
||||||
|
- ChevronDown에 `[[data-state=open]>&]:rotate-180` 트랜지션
|
||||||
|
|
||||||
|
### 토글 옵션 패턴 (Switch + 설명)
|
||||||
|
각 토글 옵션에 제목과 설명을 함께 보여준다. 사용자가 뭘 켜는 건지 이해할 수 있다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div className="min-w-0 flex-1 mr-3">
|
||||||
|
<p className="text-xs font-medium">여러 개 선택</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">한 번에 여러 값을 선택할 수 있어요</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={value} onCheckedChange={onChange} />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**규칙:**
|
||||||
|
- Checkbox가 아닌 **Switch** 사용 (토스 스타일)
|
||||||
|
- 제목: `text-xs font-medium` (Collapsible 내부) 또는 `text-sm` (독립 영역)
|
||||||
|
- 설명: `text-[10px] text-muted-foreground` 또는 `text-[11px]`
|
||||||
|
- 텍스트 블록: `min-w-0 flex-1 mr-3` (Switch와 겹치지 않게)
|
||||||
|
- Switch는 오른쪽 정렬
|
||||||
|
|
||||||
|
### 소스별 설정 영역 패턴
|
||||||
|
선택한 소스에 맞는 설정만 보여준다. 배경으로 구분한다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium">{title}</span>
|
||||||
|
</div>
|
||||||
|
{/* 소스별 설정 내용 */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 빈 상태 패턴
|
||||||
|
데이터가 없을 때 친절하게 안내한다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="text-center py-6 text-muted-foreground">
|
||||||
|
<Icon className="h-8 w-8 mx-auto mb-2 opacity-30" />
|
||||||
|
<p className="text-sm">아직 옵션이 없어요</p>
|
||||||
|
<p className="text-xs mt-0.5">위의 추가 버튼으로 옵션을 만들어보세요</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Property Row 패턴 (라벨 + 컨트롤)
|
||||||
|
간단한 설정은 수평 배치한다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="flex items-center justify-between py-1.5">
|
||||||
|
<span className="text-xs text-muted-foreground">기본 선택값</span>
|
||||||
|
<Select className="h-8 w-[160px]">...</Select>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 스크롤 제한 정책 (max-h / overflow)
|
||||||
|
|
||||||
|
### 인라인 콘텐츠에 max-h 금지 (절대 규칙!)
|
||||||
|
|
||||||
|
Collapsible이 펼쳐졌을 때 콘텐츠가 스크롤에 잘려서 보이지 않으면 안 된다.
|
||||||
|
"펼치기"의 의미는 전부 보여주는 것이다. 스크롤을 걸면 눈가리기일 뿐이다.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 금지: 펼친 콘텐츠에 스크롤 제한
|
||||||
|
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||||
|
<div className="max-h-[300px] overflow-y-auto space-y-1">
|
||||||
|
|
||||||
|
// 허용: 드롭다운/팝업 오버레이에만 max-h 사용
|
||||||
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||||
|
<PopoverContent><CommandList className="max-h-[200px]">
|
||||||
|
```
|
||||||
|
|
||||||
|
**허용 대상** (팝업 오버레이):
|
||||||
|
- `CommandGroup`, `CommandList` (Combobox 드롭다운)
|
||||||
|
- `SelectContent` (Select 드롭다운)
|
||||||
|
- `PopoverContent` 내부 리스트
|
||||||
|
|
||||||
|
**금지 대상** (인라인 콘텐츠):
|
||||||
|
- `CollapsibleContent` 내부
|
||||||
|
- 일반 `div` 안의 리스트
|
||||||
|
- 설정 패널 본문의 아이템 목록
|
||||||
|
|
||||||
|
**대안**: Collapsible 접기/펴기로 영역을 줄이되, 펼쳤을 때는 전체를 보여준다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 텍스트 오버플로우 처리 (필수!)
|
||||||
|
|
||||||
|
모든 동적 텍스트(필드명, 테이블명, 컬럼명 등)에 overflow 방지 처리가 필수.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 필수: 모든 동적 텍스트에 truncate
|
||||||
|
<p className="text-xs font-medium truncate">{fieldName}</p>
|
||||||
|
|
||||||
|
// 필수: flex 안의 텍스트 블록에 min-w-0
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate">{longText}</p>
|
||||||
|
</div>
|
||||||
|
<Switch /> {/* 고정 폭 컨트롤 */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// 필수: Switch/Select 옆의 설명 블록
|
||||||
|
<div className="min-w-0 flex-1 mr-3">
|
||||||
|
<p className="text-xs font-medium">라벨</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">설명</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**핵심 규칙:**
|
||||||
|
- `truncate` 단독 사용은 부모에 `min-w-0`이 없으면 무의미
|
||||||
|
- flex 컨테이너의 자식 중 텍스트 영역: `min-w-0 flex-1`
|
||||||
|
- 고정 크기 컨트롤 (Switch, Button, Select): `shrink-0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 컨트롤 사이즈 표준
|
||||||
|
|
||||||
|
| 컨텍스트 | 높이 | 텍스트 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| 설정 패널 내부 | `h-7` (28px) 또는 `h-8` (32px) | `text-xs` 또는 `text-sm` |
|
||||||
|
| 모달/폼 | `h-8` (32px) 또는 `h-10` (40px) | `text-sm` |
|
||||||
|
| 메인 화면 | `h-10` (40px) | `text-sm` |
|
||||||
|
|
||||||
|
설정 패널은 공간이 좁으므로 `h-7` ~ `h-8` 사용.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 설명 텍스트 톤앤매너
|
||||||
|
|
||||||
|
- **~해요** 체 사용 (토스 스타일)
|
||||||
|
- 짧고 명확하게
|
||||||
|
- 전문 용어 피하기
|
||||||
|
|
||||||
|
```
|
||||||
|
// 좋은 예
|
||||||
|
"옵션이 많을 때 검색으로 찾을 수 있어요"
|
||||||
|
"한 번에 여러 값을 선택할 수 있어요"
|
||||||
|
"선택한 값을 지울 수 있는 X 버튼이 표시돼요"
|
||||||
|
|
||||||
|
// 나쁜 예
|
||||||
|
"Searchable 모드를 활성화합니다"
|
||||||
|
"Multiple selection을 허용합니다"
|
||||||
|
"allowClear 옵션을 설정합니다"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 색상 규칙
|
||||||
|
|
||||||
|
- 선택/활성 강조: `border-primary bg-primary/5 ring-1 ring-primary/20`
|
||||||
|
- 비활성 배경: `bg-muted/30`
|
||||||
|
- 정보 텍스트: `text-muted-foreground`
|
||||||
|
- 경고: `text-amber-600`
|
||||||
|
- 성공: `text-emerald-600`
|
||||||
|
- CSS 변수 필수, 하드코딩 색상 금지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 체크리스트 (UI 작업 완료 시 확인)
|
||||||
|
|
||||||
|
- [ ] 같은 영역의 Input/Select 폭이 일치하는가?
|
||||||
|
- [ ] 카드/버튼의 텍스트가 밀리지 않고 정렬되어 있는가?
|
||||||
|
- [ ] 미작동 옵션이 표시되고 있지는 않은가?
|
||||||
|
- [ ] 고급 설정이 기본으로 접혀있는가?
|
||||||
|
- [ ] Switch에 설명 텍스트가 있는가?
|
||||||
|
- [ ] 빈 상태에 안내 메시지가 있는가?
|
||||||
|
- [ ] 전문 용어 대신 쉬운 한국어를 사용했는가?
|
||||||
|
- [ ] 다크 모드에서 정상적으로 보이는가?
|
||||||
|
- [ ] 섹션 헤더에 아이콘 + Badge 카운트가 있는가?
|
||||||
|
- [ ] 동적 텍스트에 `truncate` + 부모에 `min-w-0`이 있는가?
|
||||||
|
- [ ] 인라인 콘텐츠에 `max-h overflow-y-auto`를 사용하지 않았는가?
|
||||||
|
- [ ] Switch/Select 옆 텍스트 블록에 `min-w-0 flex-1 mr-3`이 있는가?
|
||||||
@@ -866,7 +866,7 @@ export default function DesignChangeManagementPage() {
|
|||||||
<div className="flex-1 flex flex-col overflow-hidden border rounded-lg bg-card">
|
<div className="flex-1 flex flex-col overflow-hidden border rounded-lg bg-card">
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{currentTab === "ecr" ? (
|
{currentTab === "ecr" ? (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
||||||
@@ -913,23 +913,23 @@ export default function DesignChangeManagementPage() {
|
|||||||
onClick={() => handleRowClick(item.id)}
|
onClick={() => handleRowClick(item.id)}
|
||||||
>
|
>
|
||||||
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
|
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
|
||||||
{tsEcr.isVisible("request_no") && <TableCell className="font-semibold text-primary">{item.id}</TableCell>}
|
{tsEcr.isVisible("request_no") && <TableCell style={tsEcr.thStyle("request_no")} className="font-semibold text-primary">{item.id}</TableCell>}
|
||||||
{tsEcr.isVisible("change_type") && (
|
{tsEcr.isVisible("change_type") && (
|
||||||
<TableCell className="text-center">
|
<TableCell style={tsEcr.thStyle("change_type")} className="text-center">
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getChangeTypeStyle(item.changeType))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getChangeTypeStyle(item.changeType))}>
|
||||||
{item.changeType}
|
{item.changeType}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{tsEcr.isVisible("status") && (
|
{tsEcr.isVisible("status") && (
|
||||||
<TableCell className="text-center">
|
<TableCell style={tsEcr.thStyle("status")} className="text-center">
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getEcrStatusStyle(item.status))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getEcrStatusStyle(item.status))}>
|
||||||
{item.status}
|
{item.status}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{tsEcr.isVisible("urgency") && (
|
{tsEcr.isVisible("urgency") && (
|
||||||
<TableCell className="text-center">
|
<TableCell style={tsEcr.thStyle("urgency")} className="text-center">
|
||||||
{item.urgency === "긴급" ? (
|
{item.urgency === "긴급" ? (
|
||||||
<span className="px-2 py-0.5 rounded-full text-[11px] font-medium border bg-destructive/10 text-destructive border-destructive/20">
|
<span className="px-2 py-0.5 rounded-full text-[11px] font-medium border bg-destructive/10 text-destructive border-destructive/20">
|
||||||
긴급
|
긴급
|
||||||
@@ -939,13 +939,13 @@ export default function DesignChangeManagementPage() {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{tsEcr.isVisible("target_name") && <TableCell className="font-medium">{item.target}</TableCell>}
|
{tsEcr.isVisible("target_name") && <TableCell style={tsEcr.thStyle("target_name")} className="font-medium">{item.target}</TableCell>}
|
||||||
{tsEcr.isVisible("drawing_no") && <TableCell className="text-[13px] text-muted-foreground">{item.drawingNo}</TableCell>}
|
{tsEcr.isVisible("drawing_no") && <TableCell style={tsEcr.thStyle("drawing_no")} className="text-[13px] text-muted-foreground">{item.drawingNo}</TableCell>}
|
||||||
{tsEcr.isVisible("req_dept") && <TableCell>{item.reqDept}</TableCell>}
|
{tsEcr.isVisible("req_dept") && <TableCell style={tsEcr.thStyle("req_dept")}>{item.reqDept}</TableCell>}
|
||||||
{tsEcr.isVisible("requester") && <TableCell>{item.requester}</TableCell>}
|
{tsEcr.isVisible("requester") && <TableCell style={tsEcr.thStyle("requester")}>{item.requester}</TableCell>}
|
||||||
{tsEcr.isVisible("request_date") && <TableCell>{item.date}</TableCell>}
|
{tsEcr.isVisible("request_date") && <TableCell style={tsEcr.thStyle("request_date")}>{item.date}</TableCell>}
|
||||||
{tsEcr.isVisible("ecn_no") && (
|
{tsEcr.isVisible("ecn_no") && (
|
||||||
<TableCell>
|
<TableCell style={tsEcr.thStyle("ecn_no")}>
|
||||||
{item.ecnNo ? (
|
{item.ecnNo ? (
|
||||||
<button
|
<button
|
||||||
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-info/10 text-info border border-info/20 hover:bg-info/20 transition-colors"
|
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-info/10 text-info border border-info/20 hover:bg-info/20 transition-colors"
|
||||||
@@ -967,7 +967,7 @@ export default function DesignChangeManagementPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
<TableHead className="w-[50px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
||||||
@@ -1013,22 +1013,22 @@ export default function DesignChangeManagementPage() {
|
|||||||
onClick={() => handleRowClick(item.id)}
|
onClick={() => handleRowClick(item.id)}
|
||||||
>
|
>
|
||||||
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
|
<TableCell className="text-center text-muted-foreground">{idx + 1}</TableCell>
|
||||||
{tsEcn.isVisible("ecn_no") && <TableCell className="font-semibold text-primary">{item.id}</TableCell>}
|
{tsEcn.isVisible("ecn_no") && <TableCell style={tsEcn.thStyle("ecn_no")} className="font-semibold text-primary">{item.id}</TableCell>}
|
||||||
{tsEcn.isVisible("status") && (
|
{tsEcn.isVisible("status") && (
|
||||||
<TableCell className="text-center">
|
<TableCell style={tsEcn.thStyle("status")} className="text-center">
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getEcnStatusStyle(item.status))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getEcnStatusStyle(item.status))}>
|
||||||
{item.status}
|
{item.status}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{tsEcn.isVisible("target") && <TableCell className="font-medium">{item.target}</TableCell>}
|
{tsEcn.isVisible("target") && <TableCell style={tsEcn.thStyle("target")} className="font-medium">{item.target}</TableCell>}
|
||||||
{tsEcn.isVisible("drawing_after") && <TableCell className="text-[13px] text-success font-medium">{item.drawingAfter}</TableCell>}
|
{tsEcn.isVisible("drawing_after") && <TableCell style={tsEcn.thStyle("drawing_after")} className="text-[13px] text-success font-medium">{item.drawingAfter}</TableCell>}
|
||||||
{tsEcn.isVisible("designer") && <TableCell>{item.designer}</TableCell>}
|
{tsEcn.isVisible("designer") && <TableCell style={tsEcn.thStyle("designer")}>{item.designer}</TableCell>}
|
||||||
{tsEcn.isVisible("ecn_date") && <TableCell>{item.date}</TableCell>}
|
{tsEcn.isVisible("ecn_date") && <TableCell style={tsEcn.thStyle("ecn_date")}>{item.date}</TableCell>}
|
||||||
{tsEcn.isVisible("apply_date") && <TableCell>{item.applyDate}</TableCell>}
|
{tsEcn.isVisible("apply_date") && <TableCell style={tsEcn.thStyle("apply_date")}>{item.applyDate}</TableCell>}
|
||||||
{tsEcn.isVisible("notify_depts") && <TableCell className="text-[13px] text-muted-foreground">{item.notifyDepts.join(", ")}</TableCell>}
|
{tsEcn.isVisible("notify_depts") && <TableCell style={tsEcn.thStyle("notify_depts")} className="text-[13px] text-muted-foreground">{item.notifyDepts.join(", ")}</TableCell>}
|
||||||
{tsEcn.isVisible("ecr_id") && (
|
{tsEcn.isVisible("ecr_id") && (
|
||||||
<TableCell>
|
<TableCell style={tsEcn.thStyle("ecr_id")}>
|
||||||
<button
|
<button
|
||||||
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-warning/10 text-warning border border-warning/20 hover:bg-warning/20 transition-colors"
|
className="px-2 py-0.5 rounded-full text-[11px] font-semibold bg-warning/10 text-warning border border-warning/20 hover:bg-warning/20 transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ export default function DesignRequestPage() {
|
|||||||
<span className="ml-2 text-sm text-muted-foreground">불러오는 중...</span>
|
<span className="ml-2 text-sm text-muted-foreground">불러오는 중...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
@@ -484,7 +484,7 @@ export default function DesignRequestPage() {
|
|||||||
col.key === "due_date" && "w-[85px]",
|
col.key === "due_date" && "w-[85px]",
|
||||||
col.key === "progress" && "w-[65px] text-center",
|
col.key === "progress" && "w-[65px] text-center",
|
||||||
)}
|
)}
|
||||||
style={ts.getWidth(col.key) ? { width: ts.getWidth(col.key) } : undefined}
|
style={ts.thStyle(col.key)}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -510,30 +510,30 @@ export default function DesignRequestPage() {
|
|||||||
className={cn("cursor-pointer", selectedId === item.id && "bg-accent")}
|
className={cn("cursor-pointer", selectedId === item.id && "bg-accent")}
|
||||||
onClick={() => handleRowClick(item.id)}
|
onClick={() => handleRowClick(item.id)}
|
||||||
>
|
>
|
||||||
{ts.isVisible("request_no") && <TableCell className="text-[11px] font-semibold text-primary">{item.request_no || "-"}</TableCell>}
|
{ts.isVisible("request_no") && <TableCell className="text-[11px] font-semibold text-primary" style={ts.thStyle("request_no")}>{item.request_no || "-"}</TableCell>}
|
||||||
{ts.isVisible("design_type") && (
|
{ts.isVisible("design_type") && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center" style={ts.thStyle("design_type")}>
|
||||||
{item.design_type ? (
|
{item.design_type ? (
|
||||||
<Badge className={cn("text-[9px]", TYPE_STYLES[item.design_type])}>{item.design_type}</Badge>
|
<Badge className={cn("text-[9px]", TYPE_STYLES[item.design_type])}>{item.design_type}</Badge>
|
||||||
) : "-"}
|
) : "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("status") && (
|
{ts.isVisible("status") && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center" style={ts.thStyle("status")}>
|
||||||
<Badge className={cn("text-[9px]", STATUS_STYLES[item.status])}>{item.status}</Badge>
|
<Badge className={cn("text-[9px]", STATUS_STYLES[item.status])}>{item.status}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("priority") && (
|
{ts.isVisible("priority") && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center" style={ts.thStyle("priority")}>
|
||||||
<Badge className={cn("text-[9px]", PRIORITY_STYLES[item.priority])}>{item.priority}</Badge>
|
<Badge className={cn("text-[9px]", PRIORITY_STYLES[item.priority])}>{item.priority}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("target_name") && <TableCell className="text-[13px] font-medium">{item.target_name || "-"}</TableCell>}
|
{ts.isVisible("target_name") && <TableCell className="text-[13px] font-medium" style={ts.thStyle("target_name")}>{item.target_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("customer") && <TableCell className="text-[11px]">{item.customer || "-"}</TableCell>}
|
{ts.isVisible("customer") && <TableCell className="text-[11px]" style={ts.thStyle("customer")}>{item.customer || "-"}</TableCell>}
|
||||||
{ts.isVisible("designer") && <TableCell className="text-[11px]">{item.designer || "-"}</TableCell>}
|
{ts.isVisible("designer") && <TableCell className="text-[11px]" style={ts.thStyle("designer")}>{item.designer || "-"}</TableCell>}
|
||||||
{ts.isVisible("due_date") && <TableCell className="text-[11px]">{item.due_date || "-"}</TableCell>}
|
{ts.isVisible("due_date") && <TableCell className="text-[11px]" style={ts.thStyle("due_date")}>{item.due_date || "-"}</TableCell>}
|
||||||
{ts.isVisible("progress") && (
|
{ts.isVisible("progress") && (
|
||||||
<TableCell>
|
<TableCell style={ts.thStyle("progress")}>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="h-1.5 w-12 overflow-hidden rounded-full bg-muted">
|
<div className="h-1.5 w-12 overflow-hidden rounded-full bg-muted">
|
||||||
<div className={cn("h-full rounded-full transition-all", getProgressColor(progress))} style={{ width: `${progress}%` }} />
|
<div className={cn("h-full rounded-full transition-all", getProgressColor(progress))} style={{ width: `${progress}%` }} />
|
||||||
|
|||||||
@@ -728,7 +728,7 @@ export default function DesignProjectPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
@@ -746,7 +746,7 @@ export default function DesignProjectPage() {
|
|||||||
col.key === "progress" && "w-[100px] text-center",
|
col.key === "progress" && "w-[100px] text-center",
|
||||||
col.key === "source_no" && "w-[90px]",
|
col.key === "source_no" && "w-[90px]",
|
||||||
)}
|
)}
|
||||||
style={ts.getWidth(col.key) ? { width: ts.getWidth(col.key) } : undefined}
|
style={ts.thStyle(col.key)}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -794,7 +794,7 @@ export default function DesignProjectPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ts.isVisible("project_no") && (
|
{ts.isVisible("project_no") && (
|
||||||
<TableCell>
|
<TableCell style={ts.thStyle("project_no")}>
|
||||||
<div className="flex items-center gap-1" style={{ paddingLeft: depth * 20 }}>
|
<div className="flex items-center gap-1" style={{ paddingLeft: depth * 20 }}>
|
||||||
{hasChildren ? (
|
{hasChildren ? (
|
||||||
<button
|
<button
|
||||||
@@ -818,14 +818,14 @@ export default function DesignProjectPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("status") && (
|
{ts.isVisible("status") && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center" style={ts.thStyle("status")}>
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getStatusColor(p.status))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium border", getStatusColor(p.status))}>
|
||||||
{p.status}
|
{p.status}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("name") && (
|
{ts.isVisible("name") && (
|
||||||
<TableCell className="font-medium text-sm">
|
<TableCell className="font-medium text-sm" style={ts.thStyle("name")}>
|
||||||
{p.name}
|
{p.name}
|
||||||
{childCount > 0 && (
|
{childCount > 0 && (
|
||||||
<Badge variant="outline" className="ml-1.5 text-[10px] py-0 px-1.5 font-normal">
|
<Badge variant="outline" className="ml-1.5 text-[10px] py-0 px-1.5 font-normal">
|
||||||
@@ -834,12 +834,12 @@ export default function DesignProjectPage() {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("pm") && <TableCell className="text-[13px]">{p.pm}</TableCell>}
|
{ts.isVisible("pm") && <TableCell className="text-[13px]" style={ts.thStyle("pm")}>{p.pm}</TableCell>}
|
||||||
{ts.isVisible("customer") && <TableCell className="text-[13px]">{p.customer}</TableCell>}
|
{ts.isVisible("customer") && <TableCell className="text-[13px]" style={ts.thStyle("customer")}>{p.customer}</TableCell>}
|
||||||
{ts.isVisible("start_date") && <TableCell className="text-[13px] text-muted-foreground">{p.startDate}</TableCell>}
|
{ts.isVisible("start_date") && <TableCell className="text-[13px] text-muted-foreground" style={ts.thStyle("start_date")}>{p.startDate}</TableCell>}
|
||||||
{ts.isVisible("end_date") && <TableCell className="text-[13px] text-muted-foreground">{p.endDate}</TableCell>}
|
{ts.isVisible("end_date") && <TableCell className="text-[13px] text-muted-foreground" style={ts.thStyle("end_date")}>{p.endDate}</TableCell>}
|
||||||
{ts.isVisible("progress") && (
|
{ts.isVisible("progress") && (
|
||||||
<TableCell>
|
<TableCell style={ts.thStyle("progress")}>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
||||||
<div className={cn("h-full rounded-full transition-all", progressColor(p.progress))} style={{ width: `${p.progress}%` }} />
|
<div className={cn("h-full rounded-full transition-all", progressColor(p.progress))} style={{ width: `${p.progress}%` }} />
|
||||||
@@ -848,7 +848,7 @@ export default function DesignProjectPage() {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("source_no") && <TableCell className="text-[13px] text-muted-foreground">{p.sourceNo || "-"}</TableCell>}
|
{ts.isVisible("source_no") && <TableCell className="text-[13px] text-muted-foreground" style={ts.thStyle("source_no")}>{p.sourceNo || "-"}</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -750,7 +750,7 @@ export default function DesignTaskManagementPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
@@ -769,7 +769,7 @@ export default function DesignTaskManagementPage() {
|
|||||||
col.key === "due_date" && "w-[100px]",
|
col.key === "due_date" && "w-[100px]",
|
||||||
col.key === "designer" && "w-[80px]",
|
col.key === "designer" && "w-[80px]",
|
||||||
)}
|
)}
|
||||||
style={ts.getWidth(col.key) ? { width: ts.getWidth(col.key) } : undefined}
|
style={ts.thStyle(col.key)}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -810,38 +810,38 @@ export default function DesignTaskManagementPage() {
|
|||||||
onClick={() => handleSelectTask(item.dbId)}
|
onClick={() => handleSelectTask(item.dbId)}
|
||||||
>
|
>
|
||||||
{ts.isVisible("source_type") && (
|
{ts.isVisible("source_type") && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center" style={ts.thStyle("source_type")}>
|
||||||
<Badge variant="outline" className={cn("text-[10px] font-bold", getSourceBadge(item.sourceType))}>
|
<Badge variant="outline" className={cn("text-[10px] font-bold", getSourceBadge(item.sourceType))}>
|
||||||
{item.sourceType === "dr" ? "DR" : "ECR"}
|
{item.sourceType === "dr" ? "DR" : "ECR"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("request_no") && (
|
{ts.isVisible("request_no") && (
|
||||||
<TableCell className={cn("text-[13px] font-semibold", item.sourceType === "dr" ? "text-primary" : "text-secondary-foreground")}>
|
<TableCell className={cn("text-[13px] font-semibold", item.sourceType === "dr" ? "text-primary" : "text-secondary-foreground")} style={ts.thStyle("request_no")}>
|
||||||
{item.id}
|
{item.id}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("status") && (
|
{ts.isVisible("status") && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center" style={ts.thStyle("status")}>
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getStatusVariant(item.status))}>
|
<Badge variant="outline" className={cn("text-[10px]", getStatusVariant(item.status))}>
|
||||||
{item.status}
|
{item.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("priority") && (
|
{ts.isVisible("priority") && (
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center" style={ts.thStyle("priority")}>
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getPriorityVariant(item.priority))}>
|
<Badge variant="outline" className={cn("text-[10px]", getPriorityVariant(item.priority))}>
|
||||||
{item.priority}
|
{item.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("target_name") && <TableCell className="max-w-[200px] truncate text-[13px] font-medium">{item.targetName}</TableCell>}
|
{ts.isVisible("target_name") && <TableCell className="max-w-[200px] truncate text-[13px] font-medium" style={ts.thStyle("target_name")}>{item.targetName}</TableCell>}
|
||||||
{ts.isVisible("req_dept") && <TableCell className="text-[13px]">{item.reqDept}</TableCell>}
|
{ts.isVisible("req_dept") && <TableCell className="text-[13px]" style={ts.thStyle("req_dept")}>{item.reqDept}</TableCell>}
|
||||||
{ts.isVisible("requester") && <TableCell className="text-[13px]">{item.requester}</TableCell>}
|
{ts.isVisible("requester") && <TableCell className="text-[13px]" style={ts.thStyle("requester")}>{item.requester}</TableCell>}
|
||||||
{ts.isVisible("request_date") && <TableCell className="text-[13px]">{item.date}</TableCell>}
|
{ts.isVisible("request_date") && <TableCell className="text-[13px]" style={ts.thStyle("request_date")}>{item.date}</TableCell>}
|
||||||
{ts.isVisible("due_date") && <TableCell className="text-[13px]">{item.dueDate}</TableCell>}
|
{ts.isVisible("due_date") && <TableCell className="text-[13px]" style={ts.thStyle("due_date")}>{item.dueDate}</TableCell>}
|
||||||
{ts.isVisible("designer") && (
|
{ts.isVisible("designer") && (
|
||||||
<TableCell className="text-[13px]">
|
<TableCell className="text-[13px]" style={ts.thStyle("designer")}>
|
||||||
{item.designer || <span className="text-muted-foreground">미배정</span>}
|
{item.designer || <span className="text-muted-foreground">미배정</span>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -442,15 +442,15 @@ export default function EquipmentInfoPage() {
|
|||||||
<p className="text-sm">등록된 설비가 없어요</p>
|
<p className="text-sm">등록된 설비가 없어요</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table noWrapper>
|
<Table noWrapper style={{ tableLayout: "fixed" }}>
|
||||||
<thead className="sticky top-0 z-10 bg-card">
|
<thead className="sticky top-0 z-10 bg-card">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{ts.isVisible("equipment_code") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비코드</TableHead>}
|
{ts.isVisible("equipment_code") && <TableHead style={ts.thStyle("equipment_code")} className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비코드</TableHead>}
|
||||||
{ts.isVisible("equipment_name") && <TableHead className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비명</TableHead>}
|
{ts.isVisible("equipment_name") && <TableHead style={ts.thStyle("equipment_name")} className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비명</TableHead>}
|
||||||
{ts.isVisible("equipment_type") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비유형</TableHead>}
|
{ts.isVisible("equipment_type") && <TableHead style={ts.thStyle("equipment_type")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비유형</TableHead>}
|
||||||
{ts.isVisible("manufacturer") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">제조사</TableHead>}
|
{ts.isVisible("manufacturer") && <TableHead style={ts.thStyle("manufacturer")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">제조사</TableHead>}
|
||||||
{ts.isVisible("installation_location") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설치장소</TableHead>}
|
{ts.isVisible("installation_location") && <TableHead style={ts.thStyle("installation_location")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설치장소</TableHead>}
|
||||||
{ts.isVisible("operation_status") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">가동상태</TableHead>}
|
{ts.isVisible("operation_status") && <TableHead style={ts.thStyle("operation_status")} className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">가동상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</thead>
|
</thead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -461,12 +461,12 @@ export default function EquipmentInfoPage() {
|
|||||||
onClick={() => setSelectedEquipId(equip.id)}
|
onClick={() => setSelectedEquipId(equip.id)}
|
||||||
onDoubleClick={openEquipEdit}
|
onDoubleClick={openEquipEdit}
|
||||||
>
|
>
|
||||||
{ts.isVisible("equipment_code") && <TableCell className="text-[13px] font-mono">{equip.equipment_code}</TableCell>}
|
{ts.isVisible("equipment_code") && <TableCell style={ts.thStyle("equipment_code")} className="text-[13px] font-mono">{equip.equipment_code}</TableCell>}
|
||||||
{ts.isVisible("equipment_name") && <TableCell className="text-sm max-w-[150px] truncate" title={equip.equipment_name}>{equip.equipment_name || "-"}</TableCell>}
|
{ts.isVisible("equipment_name") && <TableCell style={ts.thStyle("equipment_name")} className="text-sm max-w-[150px] truncate" title={equip.equipment_name}>{equip.equipment_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("equipment_type") && <TableCell className="text-[13px]">{equip.equipment_type || "-"}</TableCell>}
|
{ts.isVisible("equipment_type") && <TableCell style={ts.thStyle("equipment_type")} className="text-[13px]">{equip.equipment_type || "-"}</TableCell>}
|
||||||
{ts.isVisible("manufacturer") && <TableCell className="text-[13px]">{equip.manufacturer || "-"}</TableCell>}
|
{ts.isVisible("manufacturer") && <TableCell style={ts.thStyle("manufacturer")} className="text-[13px]">{equip.manufacturer || "-"}</TableCell>}
|
||||||
{ts.isVisible("installation_location") && <TableCell className="text-[13px]">{equip.installation_location || "-"}</TableCell>}
|
{ts.isVisible("installation_location") && <TableCell style={ts.thStyle("installation_location")} className="text-[13px]">{equip.installation_location || "-"}</TableCell>}
|
||||||
{ts.isVisible("operation_status") && <TableCell className="text-[13px]">{equip.operation_status || "-"}</TableCell>}
|
{ts.isVisible("operation_status") && <TableCell style={ts.thStyle("operation_status")} className="text-[13px]">{equip.operation_status || "-"}</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export default function PlcSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-10">
|
<TableHead className="w-10">
|
||||||
@@ -295,7 +295,7 @@ export default function PlcSettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -315,7 +315,7 @@ export default function PlcSettingsPage() {
|
|||||||
<Checkbox checked={dtChecked.includes(row.id)} onCheckedChange={(v) => setDtChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} />
|
<Checkbox checked={dtChecked.includes(row.id)} onCheckedChange={(v) => setDtChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableCell key={col.key} className={col.key === "is_active" ? "text-center" : ""}>
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className={col.key === "is_active" ? "text-center" : ""}>
|
||||||
{col.key === "is_active"
|
{col.key === "is_active"
|
||||||
? <Badge variant={row.is_active ? "default" : "secondary"} className="text-xs">{row.is_active ? "사용" : "미사용"}</Badge>
|
? <Badge variant={row.is_active ? "default" : "secondary"} className="text-xs">{row.is_active ? "사용" : "미사용"}</Badge>
|
||||||
: row[col.key] ?? ""}
|
: row[col.key] ?? ""}
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ export default function LogisticsInfoPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] p-2">
|
<TableHead className="w-[40px] p-2">
|
||||||
@@ -792,11 +792,7 @@ export default function LogisticsInfoPage() {
|
|||||||
col.align === "right" && "text-right",
|
col.align === "right" && "text-right",
|
||||||
col.align === "center" && "text-center"
|
col.align === "center" && "text-center"
|
||||||
)}
|
)}
|
||||||
style={
|
style={tsMap[tab.key].thStyle(col.key)}
|
||||||
col.width
|
|
||||||
? { width: col.width, minWidth: col.width }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -838,6 +834,7 @@ export default function LogisticsInfoPage() {
|
|||||||
col.align === "right" && "text-right",
|
col.align === "right" && "text-right",
|
||||||
col.align === "center" && "text-center"
|
col.align === "center" && "text-center"
|
||||||
)}
|
)}
|
||||||
|
style={tsMap[tab.key].thStyle(col.key)}
|
||||||
>
|
>
|
||||||
{display}
|
{display}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -378,13 +378,14 @@ export default function InventoryStatusPage() {
|
|||||||
등록된 재고가 없어요
|
등록된 재고가 없어요
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
|
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={col.key}
|
key={col.key}
|
||||||
|
style={ts.thStyle(col.key)}
|
||||||
className={cn(col.align === "right" && "text-right")}
|
className={cn(col.align === "right" && "text-right")}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
@@ -409,7 +410,7 @@ export default function InventoryStatusPage() {
|
|||||||
{ts.visibleColumns.map((col) => {
|
{ts.visibleColumns.map((col) => {
|
||||||
if (col.key === "current_qty") {
|
if (col.key === "current_qty") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="text-right font-mono">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="text-right font-mono">
|
||||||
<span className={cn(item._isLow && "text-destructive font-bold")}>
|
<span className={cn(item._isLow && "text-destructive font-bold")}>
|
||||||
{Number(item.current_qty || 0).toLocaleString()}
|
{Number(item.current_qty || 0).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
@@ -421,14 +422,14 @@ export default function InventoryStatusPage() {
|
|||||||
}
|
}
|
||||||
if (col.key === "safety_qty") {
|
if (col.key === "safety_qty") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="text-right font-mono">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="text-right font-mono">
|
||||||
{Number(item.safety_qty || 0).toLocaleString()}
|
{Number(item.safety_qty || 0).toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (col.key === "status") {
|
if (col.key === "status") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key} style={ts.thStyle(col.key)}>
|
||||||
<Badge variant={getStatusVariant(item.status)} className="text-[10px]">
|
<Badge variant={getStatusVariant(item.status)} className="text-[10px]">
|
||||||
{item.status}
|
{item.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -436,7 +437,7 @@ export default function InventoryStatusPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="truncate max-w-[150px]">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="truncate max-w-[150px]">
|
||||||
{item[col.key] ?? ""}
|
{item[col.key] ?? ""}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ export default function OutboundPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] text-center text-[11px] uppercase tracking-wide">
|
<TableHead className="w-[40px] text-center text-[11px] uppercase tracking-wide">
|
||||||
@@ -613,21 +613,21 @@ export default function OutboundPage() {
|
|||||||
onCheckedChange={toggleCheckAll}
|
onCheckedChange={toggleCheckAll}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.isVisible("outbound_number") && <TableHead className="w-[130px] text-[11px] uppercase tracking-wide">출고번호</TableHead>}
|
{ts.isVisible("outbound_number") && <TableHead style={ts.thStyle("outbound_number")} className="w-[130px] text-[11px] uppercase tracking-wide">출고번호</TableHead>}
|
||||||
{ts.isVisible("outbound_type") && <TableHead className="w-[90px] text-[11px] uppercase tracking-wide">출고유형</TableHead>}
|
{ts.isVisible("outbound_type") && <TableHead style={ts.thStyle("outbound_type")} className="w-[90px] text-[11px] uppercase tracking-wide">출고유형</TableHead>}
|
||||||
{ts.isVisible("outbound_date") && <TableHead className="w-[100px] text-[11px] uppercase tracking-wide">출고일</TableHead>}
|
{ts.isVisible("outbound_date") && <TableHead style={ts.thStyle("outbound_date")} className="w-[100px] text-[11px] uppercase tracking-wide">출고일</TableHead>}
|
||||||
{ts.isVisible("reference_number") && <TableHead className="w-[120px] text-[11px] uppercase tracking-wide">참조번호</TableHead>}
|
{ts.isVisible("reference_number") && <TableHead style={ts.thStyle("reference_number")} className="w-[120px] text-[11px] uppercase tracking-wide">참조번호</TableHead>}
|
||||||
{ts.isVisible("source_type") && <TableHead className="w-[80px] text-[11px] uppercase tracking-wide">데이터출처</TableHead>}
|
{ts.isVisible("source_type") && <TableHead style={ts.thStyle("source_type")} className="w-[80px] text-[11px] uppercase tracking-wide">데이터출처</TableHead>}
|
||||||
{ts.isVisible("customer_name") && <TableHead className="w-[120px] text-[11px] uppercase tracking-wide">거래처</TableHead>}
|
{ts.isVisible("customer_name") && <TableHead style={ts.thStyle("customer_name")} className="w-[120px] text-[11px] uppercase tracking-wide">거래처</TableHead>}
|
||||||
{ts.isVisible("item_number") && <TableHead className="w-[100px] text-[11px] uppercase tracking-wide">품목코드</TableHead>}
|
{ts.isVisible("item_number") && <TableHead style={ts.thStyle("item_number")} className="w-[100px] text-[11px] uppercase tracking-wide">품목코드</TableHead>}
|
||||||
{ts.isVisible("item_name") && <TableHead className="min-w-[150px] text-[11px] uppercase tracking-wide">품목명</TableHead>}
|
{ts.isVisible("item_name") && <TableHead style={ts.thStyle("item_name")} className="min-w-[150px] text-[11px] uppercase tracking-wide">품목명</TableHead>}
|
||||||
{ts.isVisible("spec") && <TableHead className="w-[80px] text-[11px] uppercase tracking-wide">규격</TableHead>}
|
{ts.isVisible("spec") && <TableHead style={ts.thStyle("spec")} className="w-[80px] text-[11px] uppercase tracking-wide">규격</TableHead>}
|
||||||
{ts.isVisible("outbound_qty") && <TableHead className="w-[80px] text-right text-[11px] uppercase tracking-wide">출고수량</TableHead>}
|
{ts.isVisible("outbound_qty") && <TableHead style={ts.thStyle("outbound_qty")} className="w-[80px] text-right text-[11px] uppercase tracking-wide">출고수량</TableHead>}
|
||||||
{ts.isVisible("unit_price") && <TableHead className="w-[90px] text-right text-[11px] uppercase tracking-wide">단가</TableHead>}
|
{ts.isVisible("unit_price") && <TableHead style={ts.thStyle("unit_price")} className="w-[90px] text-right text-[11px] uppercase tracking-wide">단가</TableHead>}
|
||||||
{ts.isVisible("total_amount") && <TableHead className="w-[100px] text-right text-[11px] uppercase tracking-wide">금액</TableHead>}
|
{ts.isVisible("total_amount") && <TableHead style={ts.thStyle("total_amount")} className="w-[100px] text-right text-[11px] uppercase tracking-wide">금액</TableHead>}
|
||||||
{ts.isVisible("warehouse_name") && <TableHead className="w-[100px] text-[11px] uppercase tracking-wide">창고</TableHead>}
|
{ts.isVisible("warehouse_name") && <TableHead style={ts.thStyle("warehouse_name")} className="w-[100px] text-[11px] uppercase tracking-wide">창고</TableHead>}
|
||||||
{ts.isVisible("outbound_status") && <TableHead className="w-[90px] text-center text-[11px] uppercase tracking-wide">출고상태</TableHead>}
|
{ts.isVisible("outbound_status") && <TableHead style={ts.thStyle("outbound_status")} className="w-[90px] text-center text-[11px] uppercase tracking-wide">출고상태</TableHead>}
|
||||||
{ts.isVisible("remark") && <TableHead className="w-[100px] text-[11px] uppercase tracking-wide">비고</TableHead>}
|
{ts.isVisible("remark") && <TableHead style={ts.thStyle("remark")} className="w-[100px] text-[11px] uppercase tracking-wide">비고</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -672,10 +672,10 @@ export default function OutboundPage() {
|
|||||||
onCheckedChange={() => toggleCheck(row.id)}
|
onCheckedChange={() => toggleCheck(row.id)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.isVisible("outbound_number") && <TableCell className="max-w-[130px] truncate font-medium" title={row.outbound_number}>
|
{ts.isVisible("outbound_number") && <TableCell style={ts.thStyle("outbound_number")} className="max-w-[130px] truncate font-medium" title={row.outbound_number}>
|
||||||
{row.outbound_number}
|
{row.outbound_number}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("outbound_type") && <TableCell>
|
{ts.isVisible("outbound_type") && <TableCell style={ts.thStyle("outbound_type")}>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn("text-[11px]", getTypeColor(row.outbound_type))}
|
className={cn("text-[11px]", getTypeColor(row.outbound_type))}
|
||||||
@@ -683,40 +683,40 @@ export default function OutboundPage() {
|
|||||||
{row.outbound_type || "-"}
|
{row.outbound_type || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("outbound_date") && <TableCell className="text-[13px]">
|
{ts.isVisible("outbound_date") && <TableCell style={ts.thStyle("outbound_date")} className="text-[13px]">
|
||||||
{row.outbound_date
|
{row.outbound_date
|
||||||
? new Date(row.outbound_date).toLocaleDateString("ko-KR")
|
? new Date(row.outbound_date).toLocaleDateString("ko-KR")
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("reference_number") && <TableCell className="max-w-[120px] truncate text-[13px]" title={row.reference_number || "-"}>
|
{ts.isVisible("reference_number") && <TableCell style={ts.thStyle("reference_number")} className="max-w-[120px] truncate text-[13px]" title={row.reference_number || "-"}>
|
||||||
{row.reference_number || "-"}
|
{row.reference_number || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("source_type") && <TableCell className="text-[13px]">
|
{ts.isVisible("source_type") && <TableCell style={ts.thStyle("source_type")} className="text-[13px]">
|
||||||
{row.source_type
|
{row.source_type
|
||||||
? SOURCE_TYPE_LABEL[row.source_type] || row.source_type
|
? SOURCE_TYPE_LABEL[row.source_type] || row.source_type
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("customer_name") && <TableCell className="max-w-[120px] truncate text-[13px]" title={row.customer_name || "-"}>
|
{ts.isVisible("customer_name") && <TableCell style={ts.thStyle("customer_name")} className="max-w-[120px] truncate text-[13px]" title={row.customer_name || "-"}>
|
||||||
{row.customer_name || "-"}
|
{row.customer_name || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("item_number") && <TableCell className="max-w-[130px] truncate text-[13px]" title={row.item_code || "-"}>
|
{ts.isVisible("item_number") && <TableCell style={ts.thStyle("item_number")} className="max-w-[130px] truncate text-[13px]" title={row.item_code || "-"}>
|
||||||
{row.item_code || "-"}
|
{row.item_code || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("item_name") && <TableCell className="max-w-[150px] truncate text-[13px]" title={row.item_name || "-"}>{row.item_name || "-"}</TableCell>}
|
{ts.isVisible("item_name") && <TableCell style={ts.thStyle("item_name")} className="max-w-[150px] truncate text-[13px]" title={row.item_name || "-"}>{row.item_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("spec") && <TableCell className="max-w-[100px] truncate text-[13px]" title={row.specification || "-"}>{row.specification || "-"}</TableCell>}
|
{ts.isVisible("spec") && <TableCell style={ts.thStyle("spec")} className="max-w-[100px] truncate text-[13px]" title={row.specification || "-"}>{row.specification || "-"}</TableCell>}
|
||||||
{ts.isVisible("outbound_qty") && <TableCell className="text-right font-mono text-[13px] font-semibold">
|
{ts.isVisible("outbound_qty") && <TableCell style={ts.thStyle("outbound_qty")} className="text-right font-mono text-[13px] font-semibold">
|
||||||
{Number(row.outbound_qty || 0).toLocaleString()}
|
{Number(row.outbound_qty || 0).toLocaleString()}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("unit_price") && <TableCell className="text-right font-mono text-[13px]">
|
{ts.isVisible("unit_price") && <TableCell style={ts.thStyle("unit_price")} className="text-right font-mono text-[13px]">
|
||||||
{Number(row.unit_price || 0).toLocaleString()}
|
{Number(row.unit_price || 0).toLocaleString()}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("total_amount") && <TableCell className="text-right font-mono text-[13px] font-semibold">
|
{ts.isVisible("total_amount") && <TableCell style={ts.thStyle("total_amount")} className="text-right font-mono text-[13px] font-semibold">
|
||||||
{Number(row.total_amount || 0).toLocaleString()}
|
{Number(row.total_amount || 0).toLocaleString()}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("warehouse_name") && <TableCell className="text-[13px]">
|
{ts.isVisible("warehouse_name") && <TableCell style={ts.thStyle("warehouse_name")} className="text-[13px]">
|
||||||
{row.warehouse_name || row.warehouse_code || "-"}
|
{row.warehouse_name || row.warehouse_code || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("outbound_status") && <TableCell className="text-center">
|
{ts.isVisible("outbound_status") && <TableCell style={ts.thStyle("outbound_status")} className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -727,7 +727,7 @@ export default function OutboundPage() {
|
|||||||
{row.outbound_status || "-"}
|
{row.outbound_status || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("remark") && <TableCell className="max-w-[120px] truncate text-[13px]" title={row.memo || "-"}>
|
{ts.isVisible("remark") && <TableCell style={ts.thStyle("remark")} className="max-w-[120px] truncate text-[13px]" title={row.memo || "-"}>
|
||||||
{row.memo || "-"}
|
{row.memo || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -458,15 +458,15 @@ export default function PackagingPage() {
|
|||||||
<div className="flex flex-1 flex-col overflow-hidden rounded-lg border bg-card">
|
<div className="flex flex-1 flex-col overflow-hidden rounded-lg border bg-card">
|
||||||
{/* 포장재 목록 테이블 */}
|
{/* 포장재 목록 테이블 */}
|
||||||
<div className={cn("overflow-auto", selectedPkg ? "flex-[0_0_50%] border-b" : "flex-1")}>
|
<div className={cn("overflow-auto", selectedPkg ? "flex-[0_0_50%] border-b" : "flex-1")}>
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
{ts.isVisible("pkg_code") && <TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>}
|
{ts.isVisible("pkg_code") && <TableHead style={ts.thStyle("pkg_code")} className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>}
|
||||||
{ts.isVisible("pkg_name") && <TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">포장명</TableHead>}
|
{ts.isVisible("pkg_name") && <TableHead style={ts.thStyle("pkg_name")} className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">포장명</TableHead>}
|
||||||
{ts.isVisible("pkg_type") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">유형</TableHead>}
|
{ts.isVisible("pkg_type") && <TableHead style={ts.thStyle("pkg_type")} className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">유형</TableHead>}
|
||||||
{ts.isVisible("size") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">크기(mm)</TableHead>}
|
{ts.isVisible("size") && <TableHead style={ts.thStyle("size")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">크기(mm)</TableHead>}
|
||||||
{ts.isVisible("max_weight") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">최대중량</TableHead>}
|
{ts.isVisible("max_weight") && <TableHead style={ts.thStyle("max_weight")} className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">최대중량</TableHead>}
|
||||||
{ts.isVisible("status") && <TableHead className="w-[60px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{ts.isVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[60px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -496,12 +496,12 @@ export default function PackagingPage() {
|
|||||||
)}
|
)}
|
||||||
onClick={() => selectPkg(p)}
|
onClick={() => selectPkg(p)}
|
||||||
>
|
>
|
||||||
{ts.isVisible("pkg_code") && <TableCell className="p-2 font-medium">{p.pkg_code}</TableCell>}
|
{ts.isVisible("pkg_code") && <TableCell style={ts.thStyle("pkg_code")} className="p-2 font-medium">{p.pkg_code}</TableCell>}
|
||||||
{ts.isVisible("pkg_name") && <TableCell className="p-2">{p.pkg_name}</TableCell>}
|
{ts.isVisible("pkg_name") && <TableCell style={ts.thStyle("pkg_name")} className="p-2">{p.pkg_name}</TableCell>}
|
||||||
{ts.isVisible("pkg_type") && <TableCell className="p-2">{PKG_TYPE_LABEL[p.pkg_type] || p.pkg_type || "-"}</TableCell>}
|
{ts.isVisible("pkg_type") && <TableCell style={ts.thStyle("pkg_type")} className="p-2">{PKG_TYPE_LABEL[p.pkg_type] || p.pkg_type || "-"}</TableCell>}
|
||||||
{ts.isVisible("size") && <TableCell className="p-2 text-[10px] tabular-nums">{fmtSize(p.width_mm, p.length_mm, p.height_mm)}</TableCell>}
|
{ts.isVisible("size") && <TableCell style={ts.thStyle("size")} className="p-2 text-[10px] tabular-nums">{fmtSize(p.width_mm, p.length_mm, p.height_mm)}</TableCell>}
|
||||||
{ts.isVisible("max_weight") && <TableCell className="p-2 text-right">{Number(p.max_load_kg || 0) > 0 ? `${p.max_load_kg}kg` : "-"}</TableCell>}
|
{ts.isVisible("max_weight") && <TableCell style={ts.thStyle("max_weight")} className="p-2 text-right">{Number(p.max_load_kg || 0) > 0 ? `${p.max_load_kg}kg` : "-"}</TableCell>}
|
||||||
{ts.isVisible("status") && <TableCell className="p-2 text-center">
|
{ts.isVisible("status") && <TableCell style={ts.thStyle("status")} className="p-2 text-center">
|
||||||
<span className={cn("rounded px-1.5 py-0.5 text-[10px] font-medium", getStatusColor(p.status))}>
|
<span className={cn("rounded px-1.5 py-0.5 text-[10px] font-medium", getStatusColor(p.status))}>
|
||||||
{STATUS_LABEL[p.status] || p.status}
|
{STATUS_LABEL[p.status] || p.status}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ export default function ReceivingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] text-center">
|
<TableHead className="w-[40px] text-center">
|
||||||
@@ -584,21 +584,21 @@ export default function ReceivingPage() {
|
|||||||
onCheckedChange={toggleCheckAll}
|
onCheckedChange={toggleCheckAll}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.isVisible("inbound_number") && <TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고번호</TableHead>}
|
{ts.isVisible("inbound_number") && <TableHead style={ts.thStyle("inbound_number")} className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고번호</TableHead>}
|
||||||
{ts.isVisible("inbound_type") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고유형</TableHead>}
|
{ts.isVisible("inbound_type") && <TableHead style={ts.thStyle("inbound_type")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고유형</TableHead>}
|
||||||
{ts.isVisible("inbound_date") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고일</TableHead>}
|
{ts.isVisible("inbound_date") && <TableHead style={ts.thStyle("inbound_date")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고일</TableHead>}
|
||||||
{ts.isVisible("reference_number") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">참조번호</TableHead>}
|
{ts.isVisible("reference_number") && <TableHead style={ts.thStyle("reference_number")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">참조번호</TableHead>}
|
||||||
{ts.isVisible("source_type") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">데이터출처</TableHead>}
|
{ts.isVisible("source_type") && <TableHead style={ts.thStyle("source_type")} className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">데이터출처</TableHead>}
|
||||||
{ts.isVisible("supplier_name") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급처</TableHead>}
|
{ts.isVisible("supplier_name") && <TableHead style={ts.thStyle("supplier_name")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급처</TableHead>}
|
||||||
{ts.isVisible("item_number") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>}
|
{ts.isVisible("item_number") && <TableHead style={ts.thStyle("item_number")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>}
|
||||||
{ts.isVisible("item_name") && <TableHead className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>}
|
{ts.isVisible("item_name") && <TableHead style={ts.thStyle("item_name")} className="min-w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>}
|
||||||
{ts.isVisible("spec") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
{ts.isVisible("spec") && <TableHead style={ts.thStyle("spec")} className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
||||||
{ts.isVisible("inbound_qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고수량</TableHead>}
|
{ts.isVisible("inbound_qty") && <TableHead style={ts.thStyle("inbound_qty")} className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고수량</TableHead>}
|
||||||
{ts.isVisible("unit_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단가</TableHead>}
|
{ts.isVisible("unit_price") && <TableHead style={ts.thStyle("unit_price")} className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단가</TableHead>}
|
||||||
{ts.isVisible("total_amount") && <TableHead className="w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">금액</TableHead>}
|
{ts.isVisible("total_amount") && <TableHead style={ts.thStyle("total_amount")} className="w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">금액</TableHead>}
|
||||||
{ts.isVisible("warehouse_name") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">창고</TableHead>}
|
{ts.isVisible("warehouse_name") && <TableHead style={ts.thStyle("warehouse_name")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">창고</TableHead>}
|
||||||
{ts.isVisible("inbound_status") && <TableHead className="w-[90px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고상태</TableHead>}
|
{ts.isVisible("inbound_status") && <TableHead style={ts.thStyle("inbound_status")} className="w-[90px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고상태</TableHead>}
|
||||||
{ts.isVisible("remark") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>}
|
{ts.isVisible("remark") && <TableHead style={ts.thStyle("remark")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -642,10 +642,10 @@ export default function ReceivingPage() {
|
|||||||
onCheckedChange={() => toggleCheck(row.id)}
|
onCheckedChange={() => toggleCheck(row.id)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.isVisible("inbound_number") && <TableCell className="max-w-[130px] truncate font-medium" title={row.inbound_number}>
|
{ts.isVisible("inbound_number") && <TableCell style={ts.thStyle("inbound_number")} className="max-w-[130px] truncate font-medium" title={row.inbound_number}>
|
||||||
{row.inbound_number}
|
{row.inbound_number}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("inbound_type") && <TableCell>
|
{ts.isVisible("inbound_type") && <TableCell style={ts.thStyle("inbound_type")}>
|
||||||
<Badge
|
<Badge
|
||||||
variant={getTypeVariant(row.inbound_type)}
|
variant={getTypeVariant(row.inbound_type)}
|
||||||
className="text-[11px]"
|
className="text-[11px]"
|
||||||
@@ -653,40 +653,40 @@ export default function ReceivingPage() {
|
|||||||
{row.inbound_type || "-"}
|
{row.inbound_type || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("inbound_date") && <TableCell className="text-[13px]">
|
{ts.isVisible("inbound_date") && <TableCell style={ts.thStyle("inbound_date")} className="text-[13px]">
|
||||||
{row.inbound_date
|
{row.inbound_date
|
||||||
? new Date(row.inbound_date).toLocaleDateString("ko-KR")
|
? new Date(row.inbound_date).toLocaleDateString("ko-KR")
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("reference_number") && <TableCell className="max-w-[120px] truncate text-[13px]" title={row.reference_number || "-"}>
|
{ts.isVisible("reference_number") && <TableCell style={ts.thStyle("reference_number")} className="max-w-[120px] truncate text-[13px]" title={row.reference_number || "-"}>
|
||||||
{row.reference_number || "-"}
|
{row.reference_number || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("source_type") && <TableCell className="text-[13px]">
|
{ts.isVisible("source_type") && <TableCell style={ts.thStyle("source_type")} className="text-[13px]">
|
||||||
{row.source_table
|
{row.source_table
|
||||||
? SOURCE_TABLE_LABEL[row.source_table] || row.source_table
|
? SOURCE_TABLE_LABEL[row.source_table] || row.source_table
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("supplier_name") && <TableCell className="max-w-[120px] truncate text-[13px]" title={row.supplier_name || "-"}>
|
{ts.isVisible("supplier_name") && <TableCell style={ts.thStyle("supplier_name")} className="max-w-[120px] truncate text-[13px]" title={row.supplier_name || "-"}>
|
||||||
{row.supplier_name || "-"}
|
{row.supplier_name || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("item_number") && <TableCell className="max-w-[130px] truncate text-[13px]" title={row.item_number || "-"}>
|
{ts.isVisible("item_number") && <TableCell style={ts.thStyle("item_number")} className="max-w-[130px] truncate text-[13px]" title={row.item_number || "-"}>
|
||||||
{row.item_number || "-"}
|
{row.item_number || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("item_name") && <TableCell className="max-w-[150px] truncate text-[13px]" title={row.item_name || "-"}>{row.item_name || "-"}</TableCell>}
|
{ts.isVisible("item_name") && <TableCell style={ts.thStyle("item_name")} className="max-w-[150px] truncate text-[13px]" title={row.item_name || "-"}>{row.item_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("spec") && <TableCell className="max-w-[100px] truncate text-[13px]" title={row.spec || "-"}>{row.spec || "-"}</TableCell>}
|
{ts.isVisible("spec") && <TableCell style={ts.thStyle("spec")} className="max-w-[100px] truncate text-[13px]" title={row.spec || "-"}>{row.spec || "-"}</TableCell>}
|
||||||
{ts.isVisible("inbound_qty") && <TableCell className="text-right text-[13px] font-semibold">
|
{ts.isVisible("inbound_qty") && <TableCell style={ts.thStyle("inbound_qty")} className="text-right text-[13px] font-semibold">
|
||||||
{Number(row.inbound_qty || 0).toLocaleString()}
|
{Number(row.inbound_qty || 0).toLocaleString()}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("unit_price") && <TableCell className="text-right text-[13px]">
|
{ts.isVisible("unit_price") && <TableCell style={ts.thStyle("unit_price")} className="text-right text-[13px]">
|
||||||
{Number(row.unit_price || 0).toLocaleString()}
|
{Number(row.unit_price || 0).toLocaleString()}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("total_amount") && <TableCell className="text-right text-[13px] font-semibold">
|
{ts.isVisible("total_amount") && <TableCell style={ts.thStyle("total_amount")} className="text-right text-[13px] font-semibold">
|
||||||
{Number(row.total_amount || 0).toLocaleString()}
|
{Number(row.total_amount || 0).toLocaleString()}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("warehouse_name") && <TableCell className="text-[13px]">
|
{ts.isVisible("warehouse_name") && <TableCell style={ts.thStyle("warehouse_name")} className="text-[13px]">
|
||||||
{row.warehouse_name || row.warehouse_code || "-"}
|
{row.warehouse_name || row.warehouse_code || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("inbound_status") && <TableCell className="text-center">
|
{ts.isVisible("inbound_status") && <TableCell style={ts.thStyle("inbound_status")} className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
variant={getStatusVariant(row.inbound_status)}
|
variant={getStatusVariant(row.inbound_status)}
|
||||||
className="text-[11px]"
|
className="text-[11px]"
|
||||||
@@ -694,7 +694,7 @@ export default function ReceivingPage() {
|
|||||||
{row.inbound_status || "-"}
|
{row.inbound_status || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("remark") && <TableCell className="max-w-[120px] truncate text-[13px]" title={row.memo || "-"}>
|
{ts.isVisible("remark") && <TableCell style={ts.thStyle("remark")} className="max-w-[120px] truncate text-[13px]" title={row.memo || "-"}>
|
||||||
{row.memo || "-"}
|
{row.memo || "-"}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -699,12 +699,12 @@ export default function WarehouseManagementPage() {
|
|||||||
등록된 창고가 없어요
|
등록된 창고가 없어요
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
|
<TableHead className="w-8 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -725,7 +725,7 @@ export default function WarehouseManagementPage() {
|
|||||||
{ts.visibleColumns.map((col) => {
|
{ts.visibleColumns.map((col) => {
|
||||||
if (col.key === "warehouse_type") {
|
if (col.key === "warehouse_type") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key} style={ts.thStyle(col.key)}>
|
||||||
<Badge variant={getTypeVariant(w.warehouse_type)} className="text-[10px]">
|
<Badge variant={getTypeVariant(w.warehouse_type)} className="text-[10px]">
|
||||||
{w.warehouse_type}
|
{w.warehouse_type}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -734,7 +734,7 @@ export default function WarehouseManagementPage() {
|
|||||||
}
|
}
|
||||||
if (col.key === "status") {
|
if (col.key === "status") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key} style={ts.thStyle(col.key)}>
|
||||||
<Badge variant={getStatusVariant(w.status)} className="text-[10px]">
|
<Badge variant={getStatusVariant(w.status)} className="text-[10px]">
|
||||||
{w.status}
|
{w.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -742,7 +742,7 @@ export default function WarehouseManagementPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="truncate max-w-[150px]">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="truncate max-w-[150px]">
|
||||||
{w[col.key] ?? ""}
|
{w[col.key] ?? ""}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -367,14 +367,14 @@ export default function DepartmentPage() {
|
|||||||
|
|
||||||
{/* 부서 테이블 */}
|
{/* 부서 테이블 */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table noWrapper>
|
<Table noWrapper style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
||||||
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">부서코드</TableHead>
|
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">부서코드</TableHead>
|
||||||
<TableHead className="min-w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">부서명</TableHead>
|
<TableHead className="min-w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">부서명</TableHead>
|
||||||
{isColVisible("parent_dept_code") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상위부서</TableHead>}
|
{isColVisible("parent_dept_code") && <TableHead style={ts.thStyle("parent_dept_code")} className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상위부서</TableHead>}
|
||||||
{isColVisible("status") && <TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{isColVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -403,9 +403,9 @@ export default function DepartmentPage() {
|
|||||||
<TableCell className="text-center text-[13px] text-muted-foreground px-2">{idx + 1}</TableCell>
|
<TableCell className="text-center text-[13px] text-muted-foreground px-2">{idx + 1}</TableCell>
|
||||||
<TableCell className="text-[13px] font-mono text-muted-foreground">{dept.dept_code}</TableCell>
|
<TableCell className="text-[13px] font-mono text-muted-foreground">{dept.dept_code}</TableCell>
|
||||||
<TableCell className="text-sm font-medium">{dept.dept_name}</TableCell>
|
<TableCell className="text-sm font-medium">{dept.dept_name}</TableCell>
|
||||||
{isColVisible("parent_dept_code") && <TableCell className="text-[13px] text-muted-foreground">{dept.parent_dept_code || "—"}</TableCell>}
|
{isColVisible("parent_dept_code") && <TableCell style={ts.thStyle("parent_dept_code")} className="text-[13px] text-muted-foreground">{dept.parent_dept_code || "—"}</TableCell>}
|
||||||
{isColVisible("status") && (
|
{isColVisible("status") && (
|
||||||
<TableCell className="text-[13px]">
|
<TableCell style={ts.thStyle("status")} className="text-[13px]">
|
||||||
{dept.status && (
|
{dept.status && (
|
||||||
<Badge
|
<Badge
|
||||||
variant={dept.status === "active" ? "default" : "outline"}
|
variant={dept.status === "active" ? "default" : "outline"}
|
||||||
|
|||||||
@@ -345,13 +345,14 @@ export default function ItemInfoPage() {
|
|||||||
등록된 품목이 없어요
|
등록된 품목이 없어요
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
|
<TableHead className="w-10 text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">#</TableHead>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={col.key}
|
key={col.key}
|
||||||
|
style={ts.thStyle(col.key)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"whitespace-nowrap text-xs",
|
"whitespace-nowrap text-xs",
|
||||||
col.align === "right" && "text-right"
|
col.align === "right" && "text-right"
|
||||||
@@ -377,6 +378,7 @@ export default function ItemInfoPage() {
|
|||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={col.key}
|
key={col.key}
|
||||||
|
style={ts.thStyle(col.key)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"whitespace-nowrap max-w-[160px] truncate",
|
"whitespace-nowrap max-w-[160px] truncate",
|
||||||
col.align === "right" && "text-right tabular-nums"
|
col.align === "right" && "text-right tabular-nums"
|
||||||
|
|||||||
@@ -348,17 +348,17 @@ export default function SubcontractorItemPage() {
|
|||||||
<p className="text-sm">등록된 외주품목이 없어요</p>
|
<p className="text-sm">등록된 외주품목이 없어요</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table noWrapper>
|
<Table noWrapper style={{ tableLayout: "fixed" }}>
|
||||||
<thead className="sticky top-0 z-10 bg-card">
|
<thead className="sticky top-0 z-10 bg-card">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{ts.isVisible("item_number") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
{ts.isVisible("item_number") && <TableHead style={ts.thStyle("item_number")} className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
||||||
{ts.isVisible("item_name") && <TableHead className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
{ts.isVisible("item_name") && <TableHead style={ts.thStyle("item_name")} className="min-w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
||||||
{ts.isVisible("size") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
{ts.isVisible("size") && <TableHead style={ts.thStyle("size")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
||||||
{ts.isVisible("unit") && <TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단위</TableHead>}
|
{ts.isVisible("unit") && <TableHead style={ts.thStyle("unit")} className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단위</TableHead>}
|
||||||
{ts.isVisible("standard_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기준단가</TableHead>}
|
{ts.isVisible("standard_price") && <TableHead style={ts.thStyle("standard_price")} className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기준단가</TableHead>}
|
||||||
{ts.isVisible("selling_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">판매가격</TableHead>}
|
{ts.isVisible("selling_price") && <TableHead style={ts.thStyle("selling_price")} className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">판매가격</TableHead>}
|
||||||
{ts.isVisible("currency_code") && <TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">통화</TableHead>}
|
{ts.isVisible("currency_code") && <TableHead style={ts.thStyle("currency_code")} className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">통화</TableHead>}
|
||||||
{ts.isVisible("status") && <TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{ts.isVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</thead>
|
</thead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -369,14 +369,14 @@ export default function SubcontractorItemPage() {
|
|||||||
onClick={() => setSelectedItemId(item.id)}
|
onClick={() => setSelectedItemId(item.id)}
|
||||||
onDoubleClick={openEditItem}
|
onDoubleClick={openEditItem}
|
||||||
>
|
>
|
||||||
{ts.isVisible("item_number") && <TableCell className="text-[13px] font-mono">{item.item_number}</TableCell>}
|
{ts.isVisible("item_number") && <TableCell style={ts.thStyle("item_number")} className="text-[13px] font-mono">{item.item_number}</TableCell>}
|
||||||
{ts.isVisible("item_name") && <TableCell className="text-sm max-w-[150px] truncate" title={item.item_name}>{item.item_name || "-"}</TableCell>}
|
{ts.isVisible("item_name") && <TableCell style={ts.thStyle("item_name")} className="text-sm max-w-[150px] truncate" title={item.item_name}>{item.item_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("size") && <TableCell className="text-[13px]">{item.size || "-"}</TableCell>}
|
{ts.isVisible("size") && <TableCell style={ts.thStyle("size")} className="text-[13px]">{item.size || "-"}</TableCell>}
|
||||||
{ts.isVisible("unit") && <TableCell className="text-[13px]">{item.unit || "-"}</TableCell>}
|
{ts.isVisible("unit") && <TableCell style={ts.thStyle("unit")} className="text-[13px]">{item.unit || "-"}</TableCell>}
|
||||||
{ts.isVisible("standard_price") && <TableCell className="text-[13px] text-right font-mono">{formatNum(item.standard_price)}</TableCell>}
|
{ts.isVisible("standard_price") && <TableCell style={ts.thStyle("standard_price")} className="text-[13px] text-right font-mono">{formatNum(item.standard_price)}</TableCell>}
|
||||||
{ts.isVisible("selling_price") && <TableCell className="text-[13px] text-right font-mono">{formatNum(item.selling_price)}</TableCell>}
|
{ts.isVisible("selling_price") && <TableCell style={ts.thStyle("selling_price")} className="text-[13px] text-right font-mono">{formatNum(item.selling_price)}</TableCell>}
|
||||||
{ts.isVisible("currency_code") && <TableCell className="text-[13px]">{item.currency_code || "-"}</TableCell>}
|
{ts.isVisible("currency_code") && <TableCell style={ts.thStyle("currency_code")} className="text-[13px]">{item.currency_code || "-"}</TableCell>}
|
||||||
{ts.isVisible("status") && <TableCell className="text-[13px]">{item.status || "-"}</TableCell>}
|
{ts.isVisible("status") && <TableCell style={ts.thStyle("status")} className="text-[13px]">{item.status || "-"}</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -955,7 +955,7 @@ export default function BomManagementPage() {
|
|||||||
<p className="text-xs">등록된 BOM이 없어요</p>
|
<p className="text-xs">등록된 BOM이 없어요</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[36px] text-center">
|
<TableHead className="w-[36px] text-center">
|
||||||
@@ -967,7 +967,7 @@ export default function BomManagementPage() {
|
|||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableHead key={col.key} className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-xs text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -995,15 +995,15 @@ export default function BomManagementPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.visibleColumns.map((col) => {
|
{ts.visibleColumns.map((col) => {
|
||||||
if (col.key === "item_code") {
|
if (col.key === "item_code") {
|
||||||
return <TableCell key={col.key} className="font-mono text-[13px]">{row.item_code || row.item_number || "-"}</TableCell>;
|
return <TableCell key={col.key} style={ts.thStyle(col.key)} className="font-mono text-[13px]">{row.item_code || row.item_number || "-"}</TableCell>;
|
||||||
}
|
}
|
||||||
if (col.key === "bom_type") {
|
if (col.key === "bom_type") {
|
||||||
return <TableCell key={col.key} className="text-[13px]">{BOM_TYPE_OPTIONS.find((o) => o.code === row.bom_type)?.label || row.bom_type || "-"}</TableCell>;
|
return <TableCell key={col.key} style={ts.thStyle(col.key)} className="text-[13px]">{BOM_TYPE_OPTIONS.find((o) => o.code === row.bom_type)?.label || row.bom_type || "-"}</TableCell>;
|
||||||
}
|
}
|
||||||
if (col.key === "status") {
|
if (col.key === "status") {
|
||||||
return <TableCell key={col.key}>{renderStatusBadge(row.status)}</TableCell>;
|
return <TableCell key={col.key} style={ts.thStyle(col.key)}>{renderStatusBadge(row.status)}</TableCell>;
|
||||||
}
|
}
|
||||||
return <TableCell key={col.key} className="text-[13px] max-w-[160px] truncate">{row[col.key] || "-"}</TableCell>;
|
return <TableCell key={col.key} style={ts.thStyle(col.key)} className="text-[13px] max-w-[160px] truncate">{row[col.key] || "-"}</TableCell>;
|
||||||
})}
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1019,7 +1019,7 @@ export default function ProductionPlanManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto rounded-md border">
|
<div className="overflow-x-auto rounded-md border">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[30px]">
|
<TableHead className="w-[30px]">
|
||||||
@@ -1028,15 +1028,15 @@ export default function ProductionPlanManagementPage() {
|
|||||||
<TableHead className="w-[40px]" />
|
<TableHead className="w-[40px]" />
|
||||||
<TableHead className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
<TableHead className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>
|
||||||
<TableHead className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
<TableHead className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>
|
||||||
{isColVisible("total_order_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">총수주량</TableHead>}
|
{isColVisible("total_order_qty") && <TableHead style={ts.thStyle("total_order_qty")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">총수주량</TableHead>}
|
||||||
{isColVisible("total_ship_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출고량</TableHead>}
|
{isColVisible("total_ship_qty") && <TableHead style={ts.thStyle("total_ship_qty")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출고량</TableHead>}
|
||||||
{isColVisible("total_balance_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">잔량</TableHead>}
|
{isColVisible("total_balance_qty") && <TableHead style={ts.thStyle("total_balance_qty")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">잔량</TableHead>}
|
||||||
{isColVisible("current_stock") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">현재고</TableHead>}
|
{isColVisible("current_stock") && <TableHead style={ts.thStyle("current_stock")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">현재고</TableHead>}
|
||||||
{isColVisible("safety_stock") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">안전재고</TableHead>}
|
{isColVisible("safety_stock") && <TableHead style={ts.thStyle("safety_stock")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">안전재고</TableHead>}
|
||||||
{isColVisible("existing_plan_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기생산계획량</TableHead>}
|
{isColVisible("existing_plan_qty") && <TableHead style={ts.thStyle("existing_plan_qty")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기생산계획량</TableHead>}
|
||||||
{isColVisible("in_progress_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">생산진행</TableHead>}
|
{isColVisible("in_progress_qty") && <TableHead style={ts.thStyle("in_progress_qty")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">생산진행</TableHead>}
|
||||||
{isColVisible("required_plan_qty") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">필요생산계획</TableHead>}
|
{isColVisible("required_plan_qty") && <TableHead style={ts.thStyle("required_plan_qty")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">필요생산계획</TableHead>}
|
||||||
{isColVisible("lead_time") && <TableHead className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">리드타임(일)</TableHead>}
|
{isColVisible("lead_time") && <TableHead style={ts.thStyle("lead_time")} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">리드타임(일)</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -1051,20 +1051,20 @@ export default function ProductionPlanManagementPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-[13px] text-primary" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
|
<TableCell className="text-[13px] text-primary" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
|
||||||
<TableCell className="text-[13px] text-primary" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
|
<TableCell className="text-[13px] text-primary" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
|
||||||
{isColVisible("total_order_qty") && <TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}</TableCell>}
|
{isColVisible("total_order_qty") && <TableCell style={ts.thStyle("total_order_qty")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}</TableCell>}
|
||||||
{isColVisible("total_ship_qty") && <TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}</TableCell>}
|
{isColVisible("total_ship_qty") && <TableCell style={ts.thStyle("total_ship_qty")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}</TableCell>}
|
||||||
{isColVisible("total_balance_qty") && <TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}</TableCell>}
|
{isColVisible("total_balance_qty") && <TableCell style={ts.thStyle("total_balance_qty")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}</TableCell>}
|
||||||
{isColVisible("current_stock") && <TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}</TableCell>}
|
{isColVisible("current_stock") && <TableCell style={ts.thStyle("current_stock")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}</TableCell>}
|
||||||
{isColVisible("safety_stock") && <TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}</TableCell>}
|
{isColVisible("safety_stock") && <TableCell style={ts.thStyle("safety_stock")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}</TableCell>}
|
||||||
{isColVisible("existing_plan_qty") && <TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}</TableCell>}
|
{isColVisible("existing_plan_qty") && <TableCell style={ts.thStyle("existing_plan_qty")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}</TableCell>}
|
||||||
{isColVisible("in_progress_qty") && <TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}</TableCell>}
|
{isColVisible("in_progress_qty") && <TableCell style={ts.thStyle("in_progress_qty")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}</TableCell>}
|
||||||
{isColVisible("required_plan_qty") && (
|
{isColVisible("required_plan_qty") && (
|
||||||
<TableCell className={cn("text-[13px] text-right font-bold", Number(item.required_plan_qty) > 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}>
|
<TableCell style={ts.thStyle("required_plan_qty")} className={cn("text-[13px] text-right font-bold", Number(item.required_plan_qty) > 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}>
|
||||||
{formatNumber(item.required_plan_qty)}
|
{formatNumber(item.required_plan_qty)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{isColVisible("lead_time") && (
|
{isColVisible("lead_time") && (
|
||||||
<TableCell className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>
|
<TableCell style={ts.thStyle("lead_time")} className="text-[13px] text-right text-primary" onClick={() => toggleItemExpand(item.item_code)}>
|
||||||
{Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"}
|
{Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
@@ -1084,9 +1084,9 @@ export default function ProductionPlanManagementPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{isColVisible("total_order_qty") && <TableCell className="text-[13px] text-right">{formatNumber(detail.order_qty)}</TableCell>}
|
{isColVisible("total_order_qty") && <TableCell style={ts.thStyle("total_order_qty")} className="text-[13px] text-right">{formatNumber(detail.order_qty)}</TableCell>}
|
||||||
{isColVisible("total_ship_qty") && <TableCell className="text-[13px] text-right">{formatNumber(detail.ship_qty)}</TableCell>}
|
{isColVisible("total_ship_qty") && <TableCell style={ts.thStyle("total_ship_qty")} className="text-[13px] text-right">{formatNumber(detail.ship_qty)}</TableCell>}
|
||||||
{isColVisible("total_balance_qty") && <TableCell className="text-[13px] text-right">{formatNumber(detail.balance_qty)}</TableCell>}
|
{isColVisible("total_balance_qty") && <TableCell style={ts.thStyle("total_balance_qty")} className="text-[13px] text-right">{formatNumber(detail.balance_qty)}</TableCell>}
|
||||||
<TableCell colSpan={orderColSpan - 2 - (isColVisible("total_order_qty") ? 1 : 0) - (isColVisible("total_ship_qty") ? 1 : 0) - (isColVisible("total_balance_qty") ? 1 : 0)} className="text-[13px] text-muted-foreground">
|
<TableCell colSpan={orderColSpan - 2 - (isColVisible("total_order_qty") ? 1 : 0) - (isColVisible("total_ship_qty") ? 1 : 0) - (isColVisible("total_balance_qty") ? 1 : 0)} className="text-[13px] text-muted-foreground">
|
||||||
납기일: {detail.due_date || "-"}
|
납기일: {detail.due_date || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -446,22 +446,22 @@ export default function WorkInstructionPage() {
|
|||||||
|
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
{ts.isVisible("work_instruction_no") && <TableHead className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업지시번호</TableHead>}
|
{ts.isVisible("work_instruction_no") && <TableHead style={ts.thStyle("work_instruction_no")} className="w-[150px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업지시번호</TableHead>}
|
||||||
{ts.isVisible("status") && <TableHead className="w-[70px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{ts.isVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[70px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
{ts.isVisible("progress") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">진행현황</TableHead>}
|
{ts.isVisible("progress") && <TableHead style={ts.thStyle("progress")} className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">진행현황</TableHead>}
|
||||||
{ts.isVisible("item_name") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>}
|
{ts.isVisible("item_name") && <TableHead style={ts.thStyle("item_name")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>}
|
||||||
{ts.isVisible("spec") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
{ts.isVisible("spec") && <TableHead style={ts.thStyle("spec")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
||||||
{ts.isVisible("qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>}
|
{ts.isVisible("qty") && <TableHead style={ts.thStyle("qty")} className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>}
|
||||||
{ts.isVisible("equipment") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>}
|
{ts.isVisible("equipment") && <TableHead style={ts.thStyle("equipment")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">설비</TableHead>}
|
||||||
{ts.isVisible("routing") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">라우팅</TableHead>}
|
{ts.isVisible("routing") && <TableHead style={ts.thStyle("routing")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">라우팅</TableHead>}
|
||||||
{ts.isVisible("work_team") && <TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업조</TableHead>}
|
{ts.isVisible("work_team") && <TableHead style={ts.thStyle("work_team")} className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업조</TableHead>}
|
||||||
{ts.isVisible("worker") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업자</TableHead>}
|
{ts.isVisible("worker") && <TableHead style={ts.thStyle("worker")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업자</TableHead>}
|
||||||
{ts.isVisible("start_date") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>}
|
{ts.isVisible("start_date") && <TableHead style={ts.thStyle("start_date")} className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">시작일</TableHead>}
|
||||||
{ts.isVisible("end_date") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료일</TableHead>}
|
{ts.isVisible("end_date") && <TableHead style={ts.thStyle("end_date")} className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">완료일</TableHead>}
|
||||||
{ts.isVisible("actions") && <TableHead className="w-[150px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업</TableHead>}
|
{ts.isVisible("actions") && <TableHead style={ts.thStyle("actions")} className="w-[150px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">작업</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -487,9 +487,9 @@ export default function WorkInstructionPage() {
|
|||||||
const isFirstOfGroup = Number(o.detail_seq) === 1;
|
const isFirstOfGroup = Number(o.detail_seq) === 1;
|
||||||
return (
|
return (
|
||||||
<TableRow key={`${o.wi_id}-${o.detail_id}`} className="hover:bg-muted/30">
|
<TableRow key={`${o.wi_id}-${o.detail_id}`} className="hover:bg-muted/30">
|
||||||
{ts.isVisible("work_instruction_no") && <TableCell className="font-mono text-[13px] font-medium">{getDisplayNo(o)}</TableCell>}
|
{ts.isVisible("work_instruction_no") && <TableCell style={ts.thStyle("work_instruction_no")} className="font-mono text-[13px] font-medium">{getDisplayNo(o)}</TableCell>}
|
||||||
{ts.isVisible("status") && <TableCell className="text-center"><Badge variant="outline" className={cn("text-[10px]", sBadge.cls)}>{sBadge.label}</Badge></TableCell>}
|
{ts.isVisible("status") && <TableCell style={ts.thStyle("status")} className="text-center"><Badge variant="outline" className={cn("text-[10px]", sBadge.cls)}>{sBadge.label}</Badge></TableCell>}
|
||||||
{ts.isVisible("progress") && <TableCell className="text-center">
|
{ts.isVisible("progress") && <TableCell style={ts.thStyle("progress")} className="text-center">
|
||||||
{isFirstOfGroup ? (
|
{isFirstOfGroup ? (
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<Badge variant="secondary" className={cn("text-[10px]", pBadge.cls)}>{pBadge.label}</Badge>
|
<Badge variant="secondary" className={cn("text-[10px]", pBadge.cls)}>{pBadge.label}</Badge>
|
||||||
@@ -500,11 +500,11 @@ export default function WorkInstructionPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : <span className="text-[10px] text-muted-foreground">↑</span>}
|
) : <span className="text-[10px] text-muted-foreground">↑</span>}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("item_name") && <TableCell className="text-sm">{o.item_name || o.item_number || "-"}</TableCell>}
|
{ts.isVisible("item_name") && <TableCell style={ts.thStyle("item_name")} className="text-sm">{o.item_name || o.item_number || "-"}</TableCell>}
|
||||||
{ts.isVisible("spec") && <TableCell className="text-[13px] text-muted-foreground">{o.item_spec || "-"}</TableCell>}
|
{ts.isVisible("spec") && <TableCell style={ts.thStyle("spec")} className="text-[13px] text-muted-foreground">{o.item_spec || "-"}</TableCell>}
|
||||||
{ts.isVisible("qty") && <TableCell className="text-right text-[13px] font-mono font-medium">{Number(o.detail_qty || 0).toLocaleString()}</TableCell>}
|
{ts.isVisible("qty") && <TableCell style={ts.thStyle("qty")} className="text-right text-[13px] font-mono font-medium">{Number(o.detail_qty || 0).toLocaleString()}</TableCell>}
|
||||||
{ts.isVisible("equipment") && <TableCell className="text-[13px]">{isFirstOfGroup ? (o.equipment_name || "-") : ""}</TableCell>}
|
{ts.isVisible("equipment") && <TableCell style={ts.thStyle("equipment")} className="text-[13px]">{isFirstOfGroup ? (o.equipment_name || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("routing") && <TableCell className="text-[13px]">
|
{ts.isVisible("routing") && <TableCell style={ts.thStyle("routing")} className="text-[13px]">
|
||||||
{isFirstOfGroup ? (
|
{isFirstOfGroup ? (
|
||||||
o.routing_version_id ? (
|
o.routing_version_id ? (
|
||||||
<button
|
<button
|
||||||
@@ -525,11 +525,11 @@ export default function WorkInstructionPage() {
|
|||||||
) : <span className="text-muted-foreground">-</span>
|
) : <span className="text-muted-foreground">-</span>
|
||||||
) : ""}
|
) : ""}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("work_team") && <TableCell className="text-center text-[13px]">{isFirstOfGroup ? (o.work_team || "-") : ""}</TableCell>}
|
{ts.isVisible("work_team") && <TableCell style={ts.thStyle("work_team")} className="text-center text-[13px]">{isFirstOfGroup ? (o.work_team || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("worker") && <TableCell className="text-[13px]">{isFirstOfGroup ? getWorkerName(o.worker) : ""}</TableCell>}
|
{ts.isVisible("worker") && <TableCell style={ts.thStyle("worker")} className="text-[13px]">{isFirstOfGroup ? getWorkerName(o.worker) : ""}</TableCell>}
|
||||||
{ts.isVisible("start_date") && <TableCell className="text-center text-[13px] font-mono">{isFirstOfGroup ? (o.start_date || "-") : ""}</TableCell>}
|
{ts.isVisible("start_date") && <TableCell style={ts.thStyle("start_date")} className="text-center text-[13px] font-mono">{isFirstOfGroup ? (o.start_date || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("end_date") && <TableCell className="text-center text-[13px] font-mono">{isFirstOfGroup ? (o.end_date || "-") : ""}</TableCell>}
|
{ts.isVisible("end_date") && <TableCell style={ts.thStyle("end_date")} className="text-center text-[13px] font-mono">{isFirstOfGroup ? (o.end_date || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("actions") && <TableCell className="text-center">
|
{ts.isVisible("actions") && <TableCell style={ts.thStyle("actions")} className="text-center">
|
||||||
{isFirstOfGroup && (
|
{isFirstOfGroup && (
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
<Button variant="ghost" size="sm" className="h-7 text-xs px-2" onClick={() => openEditModal(o)}><Pencil className="w-3 h-3" /> 수정</Button>
|
<Button variant="ghost" size="sm" className="h-7 text-xs px-2" onClick={() => openEditModal(o)}><Pencil className="w-3 h-3" /> 수정</Button>
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ export default function PurchaseOrderPage() {
|
|||||||
|
|
||||||
{/* 데이터 테이블 */}
|
{/* 데이터 테이블 */}
|
||||||
<div className="flex-1 overflow-auto border rounded-lg bg-card">
|
<div className="flex-1 overflow-auto border rounded-lg bg-card">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[44px] text-center">
|
<TableHead className="w-[44px] text-center">
|
||||||
@@ -651,20 +651,20 @@ export default function PurchaseOrderPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.isVisible("purchase_no") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">발주번호</TableHead>}
|
{ts.isVisible("purchase_no") && <TableHead style={ts.thStyle("purchase_no")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">발주번호</TableHead>}
|
||||||
{ts.isVisible("order_date") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">발주일</TableHead>}
|
{ts.isVisible("order_date") && <TableHead style={ts.thStyle("order_date")} className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">발주일</TableHead>}
|
||||||
{ts.isVisible("supplier_name") && <TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급업체</TableHead>}
|
{ts.isVisible("supplier_name") && <TableHead style={ts.thStyle("supplier_name")} className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급업체</TableHead>}
|
||||||
{ts.isVisible("item_code") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
{ts.isVisible("item_code") && <TableHead style={ts.thStyle("item_code")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
||||||
{ts.isVisible("item_name") && <TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
{ts.isVisible("item_name") && <TableHead style={ts.thStyle("item_name")} className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
||||||
{ts.isVisible("spec") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
{ts.isVisible("spec") && <TableHead style={ts.thStyle("spec")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
||||||
{ts.isVisible("order_qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">발주수량</TableHead>}
|
{ts.isVisible("order_qty") && <TableHead style={ts.thStyle("order_qty")} className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">발주수량</TableHead>}
|
||||||
{ts.isVisible("received_qty") && <TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고수량</TableHead>}
|
{ts.isVisible("received_qty") && <TableHead style={ts.thStyle("received_qty")} className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">입고수량</TableHead>}
|
||||||
{ts.isVisible("remain_qty") && <TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">잔량</TableHead>}
|
{ts.isVisible("remain_qty") && <TableHead style={ts.thStyle("remain_qty")} className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">잔량</TableHead>}
|
||||||
{ts.isVisible("unit_price") && <TableHead className="w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단가</TableHead>}
|
{ts.isVisible("unit_price") && <TableHead style={ts.thStyle("unit_price")} className="w-[100px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단가</TableHead>}
|
||||||
{ts.isVisible("amount") && <TableHead className="w-[110px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">금액</TableHead>}
|
{ts.isVisible("amount") && <TableHead style={ts.thStyle("amount")} className="w-[110px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">금액</TableHead>}
|
||||||
{ts.isVisible("due_date") && <TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">납기일</TableHead>}
|
{ts.isVisible("due_date") && <TableHead style={ts.thStyle("due_date")} className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">납기일</TableHead>}
|
||||||
{ts.isVisible("status") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{ts.isVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
{ts.isVisible("memo") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">메모</TableHead>}
|
{ts.isVisible("memo") && <TableHead style={ts.thStyle("memo")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">메모</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -696,20 +696,20 @@ export default function PurchaseOrderPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.isVisible("purchase_no") && <TableCell className="text-[13px] font-mono">{row.purchase_no}</TableCell>}
|
{ts.isVisible("purchase_no") && <TableCell style={ts.thStyle("purchase_no")} className="text-[13px] font-mono">{row.purchase_no}</TableCell>}
|
||||||
{ts.isVisible("order_date") && <TableCell className="text-[13px]">{row.order_date}</TableCell>}
|
{ts.isVisible("order_date") && <TableCell style={ts.thStyle("order_date")} className="text-[13px]">{row.order_date}</TableCell>}
|
||||||
{ts.isVisible("supplier_name") && <TableCell className="text-[13px] max-w-[140px]"><span className="block truncate" title={row.supplier_name}>{row.supplier_name}</span></TableCell>}
|
{ts.isVisible("supplier_name") && <TableCell style={ts.thStyle("supplier_name")} className="text-[13px] max-w-[140px]"><span className="block truncate" title={row.supplier_name}>{row.supplier_name}</span></TableCell>}
|
||||||
{ts.isVisible("item_code") && <TableCell className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>}
|
{ts.isVisible("item_code") && <TableCell style={ts.thStyle("item_code")} className="text-[13px] font-mono max-w-[120px]"><span className="block truncate" title={row.item_code}>{row.item_code}</span></TableCell>}
|
||||||
{ts.isVisible("item_name") && <TableCell className="text-[13px] max-w-[140px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>}
|
{ts.isVisible("item_name") && <TableCell style={ts.thStyle("item_name")} className="text-[13px] max-w-[140px]"><span className="block truncate" title={row.item_name}>{row.item_name}</span></TableCell>}
|
||||||
{ts.isVisible("spec") && <TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>}
|
{ts.isVisible("spec") && <TableCell style={ts.thStyle("spec")} className="text-[13px] text-muted-foreground">{row.spec}</TableCell>}
|
||||||
{ts.isVisible("order_qty") && <TableCell className="text-[13px] text-right font-mono">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("order_qty") && <TableCell style={ts.thStyle("order_qty")} className="text-[13px] text-right font-mono">{row.order_qty ? Number(row.order_qty).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("received_qty") && <TableCell className="text-[13px] text-right font-mono">{row.received_qty ? Number(row.received_qty).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("received_qty") && <TableCell style={ts.thStyle("received_qty")} className="text-[13px] text-right font-mono">{row.received_qty ? Number(row.received_qty).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("remain_qty") && <TableCell className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("remain_qty") && <TableCell style={ts.thStyle("remain_qty")} className="text-[13px] text-right font-mono">{row.remain_qty ? Number(row.remain_qty).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("unit_price") && <TableCell className="text-[13px] text-right font-mono">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("unit_price") && <TableCell style={ts.thStyle("unit_price")} className="text-[13px] text-right font-mono">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("amount") && <TableCell className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("amount") && <TableCell style={ts.thStyle("amount")} className="text-[13px] text-right font-mono font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("due_date") && <TableCell className="text-[13px]">{row.due_date}</TableCell>}
|
{ts.isVisible("due_date") && <TableCell style={ts.thStyle("due_date")} className="text-[13px]">{row.due_date}</TableCell>}
|
||||||
{ts.isVisible("status") && (
|
{ts.isVisible("status") && (
|
||||||
<TableCell>
|
<TableCell style={ts.thStyle("status")}>
|
||||||
{row.status && (
|
{row.status && (
|
||||||
<span className={cn("inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-semibold", STATUS_BADGE_CLASS[row.status] || "")}>
|
<span className={cn("inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-semibold", STATUS_BADGE_CLASS[row.status] || "")}>
|
||||||
{row.status}
|
{row.status}
|
||||||
@@ -717,7 +717,7 @@ export default function PurchaseOrderPage() {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("memo") && <TableCell className="text-[13px] text-muted-foreground max-w-[120px]"><span className="block truncate">{row.memo}</span></TableCell>}
|
{ts.isVisible("memo") && <TableCell style={ts.thStyle("memo")} className="text-[13px] text-muted-foreground max-w-[120px]"><span className="block truncate">{row.memo}</span></TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -381,15 +381,15 @@ export default function PurchaseItemPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>
|
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>
|
||||||
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>
|
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>
|
||||||
{isColVisible("size") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
{isColVisible("size") && <TableHead style={ts.thStyle("size")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
||||||
{isColVisible("unit") && <TableHead className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단위</TableHead>}
|
{isColVisible("unit") && <TableHead style={ts.thStyle("unit")} className="w-[60px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단위</TableHead>}
|
||||||
{isColVisible("standard_price") && <TableHead className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기준단가</TableHead>}
|
{isColVisible("standard_price") && <TableHead style={ts.thStyle("standard_price")} className="w-[90px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기준단가</TableHead>}
|
||||||
{isColVisible("status") && <TableHead className="w-[60px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{isColVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[60px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -409,11 +409,11 @@ export default function PurchaseItemPage() {
|
|||||||
>
|
>
|
||||||
<TableCell className="p-2 font-medium truncate max-w-[110px]">{item.item_number}</TableCell>
|
<TableCell className="p-2 font-medium truncate max-w-[110px]">{item.item_number}</TableCell>
|
||||||
<TableCell className="p-2 truncate max-w-[160px]">{item.item_name}</TableCell>
|
<TableCell className="p-2 truncate max-w-[160px]">{item.item_name}</TableCell>
|
||||||
{isColVisible("size") && <TableCell className="p-2 truncate">{item.size || "-"}</TableCell>}
|
{isColVisible("size") && <TableCell style={ts.thStyle("size")} className="p-2 truncate">{item.size || "-"}</TableCell>}
|
||||||
{isColVisible("unit") && <TableCell className="p-2">{item.unit || "-"}</TableCell>}
|
{isColVisible("unit") && <TableCell style={ts.thStyle("unit")} className="p-2">{item.unit || "-"}</TableCell>}
|
||||||
{isColVisible("standard_price") && <TableCell className="p-2 text-right">{item.standard_price ? Number(item.standard_price).toLocaleString() : "-"}</TableCell>}
|
{isColVisible("standard_price") && <TableCell style={ts.thStyle("standard_price")} className="p-2 text-right">{item.standard_price ? Number(item.standard_price).toLocaleString() : "-"}</TableCell>}
|
||||||
{isColVisible("status") && (
|
{isColVisible("status") && (
|
||||||
<TableCell className="p-2 text-center">
|
<TableCell style={ts.thStyle("status")} className="p-2 text-center">
|
||||||
<span className={cn("text-[10px] font-medium px-1.5 py-0.5 rounded",
|
<span className={cn("text-[10px] font-medium px-1.5 py-0.5 rounded",
|
||||||
item.status === "ACTIVE" || item.status === "사용" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
|
item.status === "ACTIVE" || item.status === "사용" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
|
||||||
)}>{item.status || "-"}</span>
|
)}>{item.status || "-"}</span>
|
||||||
|
|||||||
@@ -370,14 +370,14 @@ export default function SupplierManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급업체코드</TableHead>
|
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급업체코드</TableHead>
|
||||||
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급업체명</TableHead>
|
<TableHead className="p-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">공급업체명</TableHead>
|
||||||
{isColVisible("contact_person") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">담당자</TableHead>}
|
{isColVisible("contact_person") && <TableHead style={ts.thStyle("contact_person")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">담당자</TableHead>}
|
||||||
{isColVisible("contact_phone") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">연락처</TableHead>}
|
{isColVisible("contact_phone") && <TableHead style={ts.thStyle("contact_phone")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">연락처</TableHead>}
|
||||||
{isColVisible("status") && <TableHead className="w-[70px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{isColVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[70px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -397,10 +397,10 @@ export default function SupplierManagementPage() {
|
|||||||
>
|
>
|
||||||
<TableCell className="p-2 font-medium truncate max-w-[120px]">{s.supplier_code}</TableCell>
|
<TableCell className="p-2 font-medium truncate max-w-[120px]">{s.supplier_code}</TableCell>
|
||||||
<TableCell className="p-2 truncate max-w-[160px]">{s.supplier_name}</TableCell>
|
<TableCell className="p-2 truncate max-w-[160px]">{s.supplier_name}</TableCell>
|
||||||
{isColVisible("contact_person") && <TableCell className="p-2 truncate">{s.contact_person || "-"}</TableCell>}
|
{isColVisible("contact_person") && <TableCell style={ts.thStyle("contact_person")} className="p-2 truncate">{s.contact_person || "-"}</TableCell>}
|
||||||
{isColVisible("contact_phone") && <TableCell className="p-2 truncate">{s.contact_phone || "-"}</TableCell>}
|
{isColVisible("contact_phone") && <TableCell style={ts.thStyle("contact_phone")} className="p-2 truncate">{s.contact_phone || "-"}</TableCell>}
|
||||||
{isColVisible("status") && (
|
{isColVisible("status") && (
|
||||||
<TableCell className="p-2 text-center">
|
<TableCell style={ts.thStyle("status")} className="p-2 text-center">
|
||||||
<span className={cn("text-[10px] font-medium px-1.5 py-0.5 rounded",
|
<span className={cn("text-[10px] font-medium px-1.5 py-0.5 rounded",
|
||||||
s.status === "ACTIVE" || s.status === "사용" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
|
s.status === "ACTIVE" || s.status === "사용" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
|
||||||
)}>{s.status || "-"}</span>
|
)}>{s.status || "-"}</span>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -303,7 +303,7 @@ export default function ItemInspectionInfoPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-10">
|
<TableHead className="w-10">
|
||||||
@@ -313,7 +313,7 @@ export default function ItemInspectionInfoPage() {
|
|||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">{col.label}</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -333,7 +333,7 @@ export default function ItemInspectionInfoPage() {
|
|||||||
<Checkbox checked={checkedIds.includes(row.id)} onCheckedChange={(v) => setCheckedIds(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} />
|
<Checkbox checked={checkedIds.includes(row.id)} onCheckedChange={(v) => setCheckedIds(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key} style={ts.thStyle(col.key)}>
|
||||||
{col.key === "is_active"
|
{col.key === "is_active"
|
||||||
? <Badge variant={row.is_active ? "default" : "secondary"} className="text-xs">{row.is_active ? "사용" : "미사용"}</Badge>
|
? <Badge variant={row.is_active ? "default" : "secondary"} className="text-xs">{row.is_active ? "사용" : "미사용"}</Badge>
|
||||||
: row[col.key] ?? ""}
|
: row[col.key] ?? ""}
|
||||||
|
|||||||
@@ -464,12 +464,12 @@ export default function ClaimManagementPage() {
|
|||||||
|
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide">#</TableHead>
|
<TableHead className="w-[40px] text-center text-[11px] font-bold uppercase tracking-wide">#</TableHead>
|
||||||
{ts.visibleColumns.map((col) => (
|
{ts.visibleColumns.map((col) => (
|
||||||
<TableHead key={col.key} className="text-[11px] font-bold uppercase tracking-wide">
|
<TableHead key={col.key} style={ts.thStyle(col.key)} className="text-[11px] font-bold uppercase tracking-wide">
|
||||||
{col.label}
|
{col.label}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
@@ -513,7 +513,7 @@ export default function ClaimManagementPage() {
|
|||||||
{ts.visibleColumns.map((col) => {
|
{ts.visibleColumns.map((col) => {
|
||||||
if (col.key === "claim_type") {
|
if (col.key === "claim_type") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="text-center py-2">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="text-center py-2">
|
||||||
<Badge variant={getClaimTypeVariant(claim.claim_type)} className="text-[10px] px-1.5 py-0">
|
<Badge variant={getClaimTypeVariant(claim.claim_type)} className="text-[10px] px-1.5 py-0">
|
||||||
{claim.claim_type}
|
{claim.claim_type}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -522,7 +522,7 @@ export default function ClaimManagementPage() {
|
|||||||
}
|
}
|
||||||
if (col.key === "claim_status") {
|
if (col.key === "claim_status") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="text-center py-2">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="text-center py-2">
|
||||||
<Badge variant={getClaimStatusVariant(claim.claim_status)} className="text-[10px] px-1.5 py-0">
|
<Badge variant={getClaimStatusVariant(claim.claim_status)} className="text-[10px] px-1.5 py-0">
|
||||||
{claim.claim_status}
|
{claim.claim_status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -531,13 +531,13 @@ export default function ClaimManagementPage() {
|
|||||||
}
|
}
|
||||||
if (col.key === "claim_content") {
|
if (col.key === "claim_content") {
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="text-sm text-muted-foreground py-2 max-w-[200px] truncate">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="text-sm text-muted-foreground py-2 max-w-[200px] truncate">
|
||||||
{claim.claim_content}
|
{claim.claim_content}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TableCell key={col.key} className="text-sm py-2">
|
<TableCell key={col.key} style={ts.thStyle(col.key)} className="text-sm py-2">
|
||||||
{claim[col.key] ?? "-"}
|
{claim[col.key] ?? "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -915,16 +915,16 @@ export default function CustomerManagementPage() {
|
|||||||
|
|
||||||
{/* 거래처 테이블 */}
|
{/* 거래처 테이블 */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table noWrapper>
|
<Table noWrapper style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
<TableHead className="w-[40px] text-center px-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground">No</TableHead>
|
||||||
{isColumnVisible("customer_code") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처코드</TableHead>}
|
{isColumnVisible("customer_code") && <TableHead style={ts.thStyle("customer_code")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처코드</TableHead>}
|
||||||
{isColumnVisible("customer_name") && <TableHead className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처명</TableHead>}
|
{isColumnVisible("customer_name") && <TableHead style={ts.thStyle("customer_name")} className="min-w-[160px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처명</TableHead>}
|
||||||
{isColumnVisible("contact_person") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">대표자</TableHead>}
|
{isColumnVisible("contact_person") && <TableHead style={ts.thStyle("contact_person")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">대표자</TableHead>}
|
||||||
{isColumnVisible("contact_phone") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">연락처</TableHead>}
|
{isColumnVisible("contact_phone") && <TableHead style={ts.thStyle("contact_phone")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">연락처</TableHead>}
|
||||||
{isColumnVisible("division") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">유형</TableHead>}
|
{isColumnVisible("division") && <TableHead style={ts.thStyle("division")} className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">유형</TableHead>}
|
||||||
{isColumnVisible("status") && <TableHead className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{isColumnVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[70px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -951,12 +951,12 @@ export default function CustomerManagementPage() {
|
|||||||
onDoubleClick={() => { setSelectedCustomerId(c.id); openCustomerEdit(); }}
|
onDoubleClick={() => { setSelectedCustomerId(c.id); openCustomerEdit(); }}
|
||||||
>
|
>
|
||||||
<TableCell className="text-center text-[13px] text-muted-foreground px-2">{idx + 1}</TableCell>
|
<TableCell className="text-center text-[13px] text-muted-foreground px-2">{idx + 1}</TableCell>
|
||||||
{isColumnVisible("customer_code") && <TableCell className="text-[13px] font-mono text-muted-foreground">{c.customer_code}</TableCell>}
|
{isColumnVisible("customer_code") && <TableCell style={ts.thStyle("customer_code")} className="text-[13px] font-mono text-muted-foreground">{c.customer_code}</TableCell>}
|
||||||
{isColumnVisible("customer_name") && <TableCell className="text-sm font-medium">{c.customer_name}</TableCell>}
|
{isColumnVisible("customer_name") && <TableCell style={ts.thStyle("customer_name")} className="text-sm font-medium">{c.customer_name}</TableCell>}
|
||||||
{isColumnVisible("contact_person") && <TableCell className="text-[13px] text-muted-foreground">{c.contact_person}</TableCell>}
|
{isColumnVisible("contact_person") && <TableCell style={ts.thStyle("contact_person")} className="text-[13px] text-muted-foreground">{c.contact_person}</TableCell>}
|
||||||
{isColumnVisible("contact_phone") && <TableCell className="text-[13px] text-muted-foreground">{c.contact_phone}</TableCell>}
|
{isColumnVisible("contact_phone") && <TableCell style={ts.thStyle("contact_phone")} className="text-[13px] text-muted-foreground">{c.contact_phone}</TableCell>}
|
||||||
{isColumnVisible("division") && (
|
{isColumnVisible("division") && (
|
||||||
<TableCell className="text-[13px]">
|
<TableCell style={ts.thStyle("division")} className="text-[13px]">
|
||||||
{c.division && (
|
{c.division && (
|
||||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-normal">
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 font-normal">
|
||||||
{c.division}
|
{c.division}
|
||||||
@@ -965,7 +965,7 @@ export default function CustomerManagementPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{isColumnVisible("status") && (
|
{isColumnVisible("status") && (
|
||||||
<TableCell className="text-[13px]">
|
<TableCell style={ts.thStyle("status")} className="text-[13px]">
|
||||||
{c.status && (
|
{c.status && (
|
||||||
<Badge
|
<Badge
|
||||||
variant={c.status === "활성" || c.status === "거래중" ? "default" : "outline"}
|
variant={c.status === "활성" || c.status === "거래중" ? "default" : "outline"}
|
||||||
|
|||||||
@@ -635,7 +635,7 @@ export default function SalesOrderPage() {
|
|||||||
{/* 데이터 테이블 */}
|
{/* 데이터 테이블 */}
|
||||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||||
<div className="overflow-auto" style={{ maxHeight: "calc(100vh - 290px)" }}>
|
<div className="overflow-auto" style={{ maxHeight: "calc(100vh - 290px)" }}>
|
||||||
<Table noWrapper className="w-full">
|
<Table noWrapper className="w-full" style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-10 pl-4">
|
<TableHead className="w-10 pl-4">
|
||||||
@@ -646,19 +646,19 @@ export default function SalesOrderPage() {
|
|||||||
className="h-4 w-4 cursor-pointer rounded border-border accent-primary"
|
className="h-4 w-4 cursor-pointer rounded border-border accent-primary"
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.isVisible("order_no") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수주번호</TableHead>}
|
{ts.isVisible("order_no") && <TableHead style={ts.thStyle("order_no")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수주번호</TableHead>}
|
||||||
{ts.isVisible("part_code") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
{ts.isVisible("part_code") && <TableHead style={ts.thStyle("part_code")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
||||||
{ts.isVisible("part_name") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
{ts.isVisible("part_name") && <TableHead style={ts.thStyle("part_name")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
||||||
{ts.isVisible("spec") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
{ts.isVisible("spec") && <TableHead style={ts.thStyle("spec")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">규격</TableHead>}
|
||||||
{ts.isVisible("unit") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단위</TableHead>}
|
{ts.isVisible("unit") && <TableHead style={ts.thStyle("unit")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단위</TableHead>}
|
||||||
{ts.isVisible("qty") && <TableHead className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>}
|
{ts.isVisible("qty") && <TableHead style={ts.thStyle("qty")} className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>}
|
||||||
{ts.isVisible("ship_qty") && <TableHead className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출하수량</TableHead>}
|
{ts.isVisible("ship_qty") && <TableHead style={ts.thStyle("ship_qty")} className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출하수량</TableHead>}
|
||||||
{ts.isVisible("balance_qty") && <TableHead className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">잔량</TableHead>}
|
{ts.isVisible("balance_qty") && <TableHead style={ts.thStyle("balance_qty")} className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">잔량</TableHead>}
|
||||||
{ts.isVisible("unit_price") && <TableHead className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단가</TableHead>}
|
{ts.isVisible("unit_price") && <TableHead style={ts.thStyle("unit_price")} className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단가</TableHead>}
|
||||||
{ts.isVisible("amount") && <TableHead className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">금액</TableHead>}
|
{ts.isVisible("amount") && <TableHead style={ts.thStyle("amount")} className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">금액</TableHead>}
|
||||||
{ts.isVisible("currency_code") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">통화</TableHead>}
|
{ts.isVisible("currency_code") && <TableHead style={ts.thStyle("currency_code")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">통화</TableHead>}
|
||||||
{ts.isVisible("due_date") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">납기일</TableHead>}
|
{ts.isVisible("due_date") && <TableHead style={ts.thStyle("due_date")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">납기일</TableHead>}
|
||||||
{ts.isVisible("memo") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">메모</TableHead>}
|
{ts.isVisible("memo") && <TableHead style={ts.thStyle("memo")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">메모</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -695,28 +695,28 @@ export default function SalesOrderPage() {
|
|||||||
<TableCell className="pl-4">
|
<TableCell className="pl-4">
|
||||||
<input type="checkbox" checked={isChecked} readOnly className="h-4 w-4 cursor-pointer rounded accent-primary" />
|
<input type="checkbox" checked={isChecked} readOnly className="h-4 w-4 cursor-pointer rounded accent-primary" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.isVisible("order_no") && <TableCell className="font-mono text-[13px]">{row.order_no}</TableCell>}
|
{ts.isVisible("order_no") && <TableCell style={ts.thStyle("order_no")} className="font-mono text-[13px]">{row.order_no}</TableCell>}
|
||||||
{ts.isVisible("part_code") && (
|
{ts.isVisible("part_code") && (
|
||||||
<TableCell className="max-w-[120px]">
|
<TableCell style={ts.thStyle("part_code")} className="max-w-[120px]">
|
||||||
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
|
<span className="block truncate font-mono text-[13px]" title={row.part_code}>{row.part_code}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("part_name") && (
|
{ts.isVisible("part_name") && (
|
||||||
<TableCell className="max-w-[150px]">
|
<TableCell style={ts.thStyle("part_name")} className="max-w-[150px]">
|
||||||
<span className="block truncate text-sm" title={row.part_name}>{row.part_name}</span>
|
<span className="block truncate text-sm" title={row.part_name}>{row.part_name}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{ts.isVisible("spec") && <TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>}
|
{ts.isVisible("spec") && <TableCell style={ts.thStyle("spec")} className="text-[13px] text-muted-foreground">{row.spec}</TableCell>}
|
||||||
{ts.isVisible("unit") && <TableCell className="text-[13px]">{row.unit}</TableCell>}
|
{ts.isVisible("unit") && <TableCell style={ts.thStyle("unit")} className="text-[13px]">{row.unit}</TableCell>}
|
||||||
{ts.isVisible("qty") && <TableCell className="text-right font-mono text-[13px]">{row.qty ? Number(row.qty).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("qty") && <TableCell style={ts.thStyle("qty")} className="text-right font-mono text-[13px]">{row.qty ? Number(row.qty).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("ship_qty") && <TableCell className="text-right font-mono text-[13px] text-muted-foreground">{row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("ship_qty") && <TableCell style={ts.thStyle("ship_qty")} className="text-right font-mono text-[13px] text-muted-foreground">{row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("balance_qty") && <TableCell className="text-right font-mono text-[13px]">{row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("balance_qty") && <TableCell style={ts.thStyle("balance_qty")} className="text-right font-mono text-[13px]">{row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("unit_price") && <TableCell className="text-right font-mono text-[13px]">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("unit_price") && <TableCell style={ts.thStyle("unit_price")} className="text-right font-mono text-[13px]">{row.unit_price ? Number(row.unit_price).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("amount") && <TableCell className="text-right font-mono text-[13px] font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>}
|
{ts.isVisible("amount") && <TableCell style={ts.thStyle("amount")} className="text-right font-mono text-[13px] font-semibold">{row.amount ? Number(row.amount).toLocaleString() : ""}</TableCell>}
|
||||||
{ts.isVisible("currency_code") && <TableCell className="text-[13px]">{row.currency_code}</TableCell>}
|
{ts.isVisible("currency_code") && <TableCell style={ts.thStyle("currency_code")} className="text-[13px]">{row.currency_code}</TableCell>}
|
||||||
{ts.isVisible("due_date") && <TableCell className="text-[13px]">{row.due_date}</TableCell>}
|
{ts.isVisible("due_date") && <TableCell style={ts.thStyle("due_date")} className="text-[13px]">{row.due_date}</TableCell>}
|
||||||
{ts.isVisible("memo") && (
|
{ts.isVisible("memo") && (
|
||||||
<TableCell className="max-w-[100px]">
|
<TableCell style={ts.thStyle("memo")} className="max-w-[100px]">
|
||||||
<span className="block truncate text-[13px] text-muted-foreground" title={row.memo}>{row.memo}</span>
|
<span className="block truncate text-[13px] text-muted-foreground" title={row.memo}>{row.memo}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ export default function ShippingOrderPage() {
|
|||||||
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] text-center">
|
<TableHead className="w-[40px] text-center">
|
||||||
@@ -469,18 +469,18 @@ export default function ShippingOrderPage() {
|
|||||||
onCheckedChange={handleCheckAll}
|
onCheckedChange={handleCheckAll}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.isVisible("instruction_no") && <TableHead className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출하지시번호</TableHead>}
|
{ts.isVisible("instruction_no") && <TableHead style={ts.thStyle("instruction_no")} className="w-[140px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출하지시번호</TableHead>}
|
||||||
{ts.isVisible("ship_date") && <TableHead className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출하일자</TableHead>}
|
{ts.isVisible("ship_date") && <TableHead style={ts.thStyle("ship_date")} className="w-[100px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">출하일자</TableHead>}
|
||||||
{ts.isVisible("customer_name") && <TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처명</TableHead>}
|
{ts.isVisible("customer_name") && <TableHead style={ts.thStyle("customer_name")} className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처명</TableHead>}
|
||||||
{ts.isVisible("transport_company") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">운송업체</TableHead>}
|
{ts.isVisible("transport_company") && <TableHead style={ts.thStyle("transport_company")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">운송업체</TableHead>}
|
||||||
{ts.isVisible("vehicle_no") && <TableHead className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">차량번호</TableHead>}
|
{ts.isVisible("vehicle_no") && <TableHead style={ts.thStyle("vehicle_no")} className="w-[90px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">차량번호</TableHead>}
|
||||||
{ts.isVisible("driver_name") && <TableHead className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기사명</TableHead>}
|
{ts.isVisible("driver_name") && <TableHead style={ts.thStyle("driver_name")} className="w-[80px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기사명</TableHead>}
|
||||||
{ts.isVisible("status") && <TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{ts.isVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
{ts.isVisible("item_code") && <TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
{ts.isVisible("item_code") && <TableHead style={ts.thStyle("item_code")} className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품번</TableHead>}
|
||||||
{ts.isVisible("item_name") && <TableHead className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
{ts.isVisible("item_name") && <TableHead style={ts.thStyle("item_name")} className="w-[130px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품명</TableHead>}
|
||||||
{ts.isVisible("qty") && <TableHead className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>}
|
{ts.isVisible("qty") && <TableHead style={ts.thStyle("qty")} className="w-[70px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수량</TableHead>}
|
||||||
{ts.isVisible("source_type") && <TableHead className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">소스</TableHead>}
|
{ts.isVisible("source_type") && <TableHead style={ts.thStyle("source_type")} className="w-[80px] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">소스</TableHead>}
|
||||||
{ts.isVisible("remark") && <TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>}
|
{ts.isVisible("remark") && <TableHead style={ts.thStyle("remark")} className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">비고</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -516,22 +516,22 @@ export default function ShippingOrderPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.isVisible("instruction_no") && <TableCell className="font-medium text-sm">{order.instruction_no}</TableCell>}
|
{ts.isVisible("instruction_no") && <TableCell style={ts.thStyle("instruction_no")} className="font-medium text-sm">{order.instruction_no}</TableCell>}
|
||||||
{ts.isVisible("ship_date") && <TableCell className="text-center text-sm">{formatDate(order.instruction_date)}</TableCell>}
|
{ts.isVisible("ship_date") && <TableCell style={ts.thStyle("ship_date")} className="text-center text-sm">{formatDate(order.instruction_date)}</TableCell>}
|
||||||
{ts.isVisible("customer_name") && <TableCell className="text-sm">{order.customer_name || "-"}</TableCell>}
|
{ts.isVisible("customer_name") && <TableCell style={ts.thStyle("customer_name")} className="text-sm">{order.customer_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("transport_company") && <TableCell className="text-sm">{order.carrier_name || "-"}</TableCell>}
|
{ts.isVisible("transport_company") && <TableCell style={ts.thStyle("transport_company")} className="text-sm">{order.carrier_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("vehicle_no") && <TableCell className="text-sm">{order.vehicle_no || "-"}</TableCell>}
|
{ts.isVisible("vehicle_no") && <TableCell style={ts.thStyle("vehicle_no")} className="text-sm">{order.vehicle_no || "-"}</TableCell>}
|
||||||
{ts.isVisible("driver_name") && <TableCell className="text-sm">{order.driver_name || "-"}</TableCell>}
|
{ts.isVisible("driver_name") && <TableCell style={ts.thStyle("driver_name")} className="text-sm">{order.driver_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("status") && <TableCell className="text-center">
|
{ts.isVisible("status") && <TableCell style={ts.thStyle("status")} className="text-center">
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium", getStatusColor(order.status))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium", getStatusColor(order.status))}>
|
||||||
{getStatusLabel(order.status)}
|
{getStatusLabel(order.status)}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("item_code") && <TableCell className="text-sm">-</TableCell>}
|
{ts.isVisible("item_code") && <TableCell style={ts.thStyle("item_code")} className="text-sm">-</TableCell>}
|
||||||
{ts.isVisible("item_name") && <TableCell className="text-sm">-</TableCell>}
|
{ts.isVisible("item_name") && <TableCell style={ts.thStyle("item_name")} className="text-sm">-</TableCell>}
|
||||||
{ts.isVisible("qty") && <TableCell className="text-right text-sm">0</TableCell>}
|
{ts.isVisible("qty") && <TableCell style={ts.thStyle("qty")} className="text-right text-sm">0</TableCell>}
|
||||||
{ts.isVisible("source_type") && <TableCell className="text-center text-sm">-</TableCell>}
|
{ts.isVisible("source_type") && <TableCell style={ts.thStyle("source_type")} className="text-center text-sm">-</TableCell>}
|
||||||
{ts.isVisible("remark") && <TableCell className="text-[13px] text-muted-foreground truncate max-w-[100px]">{order.memo || "-"}</TableCell>}
|
{ts.isVisible("remark") && <TableCell style={ts.thStyle("remark")} className="text-[13px] text-muted-foreground truncate max-w-[100px]">{order.memo || "-"}</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -553,29 +553,29 @@ export default function ShippingOrderPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.isVisible("instruction_no") && <TableCell className="font-medium text-sm">{itemIdx === 0 ? order.instruction_no : ""}</TableCell>}
|
{ts.isVisible("instruction_no") && <TableCell style={ts.thStyle("instruction_no")} className="font-medium text-sm">{itemIdx === 0 ? order.instruction_no : ""}</TableCell>}
|
||||||
{ts.isVisible("ship_date") && <TableCell className="text-center text-sm">{itemIdx === 0 ? formatDate(order.instruction_date) : ""}</TableCell>}
|
{ts.isVisible("ship_date") && <TableCell style={ts.thStyle("ship_date")} className="text-center text-sm">{itemIdx === 0 ? formatDate(order.instruction_date) : ""}</TableCell>}
|
||||||
{ts.isVisible("customer_name") && <TableCell className="text-sm">{itemIdx === 0 ? (order.customer_name || "-") : ""}</TableCell>}
|
{ts.isVisible("customer_name") && <TableCell style={ts.thStyle("customer_name")} className="text-sm">{itemIdx === 0 ? (order.customer_name || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("transport_company") && <TableCell className="text-sm">{itemIdx === 0 ? (order.carrier_name || "-") : ""}</TableCell>}
|
{ts.isVisible("transport_company") && <TableCell style={ts.thStyle("transport_company")} className="text-sm">{itemIdx === 0 ? (order.carrier_name || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("vehicle_no") && <TableCell className="text-sm">{itemIdx === 0 ? (order.vehicle_no || "-") : ""}</TableCell>}
|
{ts.isVisible("vehicle_no") && <TableCell style={ts.thStyle("vehicle_no")} className="text-sm">{itemIdx === 0 ? (order.vehicle_no || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("driver_name") && <TableCell className="text-sm">{itemIdx === 0 ? (order.driver_name || "-") : ""}</TableCell>}
|
{ts.isVisible("driver_name") && <TableCell style={ts.thStyle("driver_name")} className="text-sm">{itemIdx === 0 ? (order.driver_name || "-") : ""}</TableCell>}
|
||||||
{ts.isVisible("status") && <TableCell className="text-center">
|
{ts.isVisible("status") && <TableCell style={ts.thStyle("status")} className="text-center">
|
||||||
{itemIdx === 0 && (
|
{itemIdx === 0 && (
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium", getStatusColor(order.status))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium", getStatusColor(order.status))}>
|
||||||
{getStatusLabel(order.status)}
|
{getStatusLabel(order.status)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("item_code") && <TableCell className="text-[13px] text-muted-foreground">{item.item_code}</TableCell>}
|
{ts.isVisible("item_code") && <TableCell style={ts.thStyle("item_code")} className="text-[13px] text-muted-foreground">{item.item_code}</TableCell>}
|
||||||
{ts.isVisible("item_name") && <TableCell className="font-medium text-sm">{item.item_name}</TableCell>}
|
{ts.isVisible("item_name") && <TableCell style={ts.thStyle("item_name")} className="font-medium text-sm">{item.item_name}</TableCell>}
|
||||||
{ts.isVisible("qty") && <TableCell className="text-right text-sm">{Number(item.order_qty || 0).toLocaleString()}</TableCell>}
|
{ts.isVisible("qty") && <TableCell style={ts.thStyle("qty")} className="text-right text-sm">{Number(item.order_qty || 0).toLocaleString()}</TableCell>}
|
||||||
{ts.isVisible("source_type") && <TableCell className="text-center">
|
{ts.isVisible("source_type") && <TableCell style={ts.thStyle("source_type")} className="text-center">
|
||||||
{(() => {
|
{(() => {
|
||||||
const b = getSourceBadge(item.source_type || "");
|
const b = getSourceBadge(item.source_type || "");
|
||||||
return <span className={cn("px-2 py-0.5 rounded-full text-[10px] font-medium", b.cls)}>{b.label}</span>;
|
return <span className={cn("px-2 py-0.5 rounded-full text-[10px] font-medium", b.cls)}>{b.label}</span>;
|
||||||
})()}
|
})()}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("remark") && <TableCell className="text-[13px] text-muted-foreground truncate max-w-[100px]">
|
{ts.isVisible("remark") && <TableCell style={ts.thStyle("remark")} className="text-[13px] text-muted-foreground truncate max-w-[100px]">
|
||||||
{itemIdx === 0 ? (order.memo || "-") : ""}
|
{itemIdx === 0 ? (order.memo || "-") : ""}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export default function ShippingPlanPage() {
|
|||||||
|
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="sticky top-0 z-10">
|
<TableHeader className="sticky top-0 z-10">
|
||||||
<TableRow className="bg-muted hover:bg-muted">
|
<TableRow className="bg-muted hover:bg-muted">
|
||||||
<TableHead className="w-[40px] text-center">
|
<TableHead className="w-[40px] text-center">
|
||||||
@@ -242,15 +242,15 @@ export default function ShippingPlanPage() {
|
|||||||
onCheckedChange={handleCheckAll}
|
onCheckedChange={handleCheckAll}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{ts.isVisible("order_no") && <TableHead className="w-[10%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수주번호</TableHead>}
|
{ts.isVisible("order_no") && <TableHead style={ts.thStyle("order_no")} className="w-[10%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수주번호</TableHead>}
|
||||||
{ts.isVisible("due_date") && <TableHead className="w-[8%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">납기일</TableHead>}
|
{ts.isVisible("due_date") && <TableHead style={ts.thStyle("due_date")} className="w-[8%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">납기일</TableHead>}
|
||||||
{ts.isVisible("customer_name") && <TableHead className="w-[12%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처</TableHead>}
|
{ts.isVisible("customer_name") && <TableHead style={ts.thStyle("customer_name")} className="w-[12%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처</TableHead>}
|
||||||
{ts.isVisible("part_code") && <TableHead className="w-[18%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>}
|
{ts.isVisible("part_code") && <TableHead style={ts.thStyle("part_code")} className="w-[18%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목코드</TableHead>}
|
||||||
{ts.isVisible("part_name") && <TableHead className="w-[18%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>}
|
{ts.isVisible("part_name") && <TableHead style={ts.thStyle("part_name")} className="w-[18%] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">품목명</TableHead>}
|
||||||
{ts.isVisible("order_qty") && <TableHead className="w-[7%] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수주수량</TableHead>}
|
{ts.isVisible("order_qty") && <TableHead style={ts.thStyle("order_qty")} className="w-[7%] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">수주수량</TableHead>}
|
||||||
{ts.isVisible("plan_qty") && <TableHead className="w-[7%] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획수량</TableHead>}
|
{ts.isVisible("plan_qty") && <TableHead style={ts.thStyle("plan_qty")} className="w-[7%] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획수량</TableHead>}
|
||||||
{ts.isVisible("plan_date") && <TableHead className="w-[8%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획일</TableHead>}
|
{ts.isVisible("plan_date") && <TableHead style={ts.thStyle("plan_date")} className="w-[8%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">계획일</TableHead>}
|
||||||
{ts.isVisible("status") && <TableHead className="w-[6%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
{ts.isVisible("status") && <TableHead style={ts.thStyle("status")} className="w-[6%] text-center text-[11px] font-bold uppercase tracking-wide text-muted-foreground">상태</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -293,21 +293,21 @@ export default function ShippingPlanPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{ts.isVisible("order_no") && <TableCell className="font-medium text-sm">
|
{ts.isVisible("order_no") && <TableCell style={ts.thStyle("order_no")} className="font-medium text-sm">
|
||||||
{planIdx === 0 ? (plan.order_no || "-") : ""}
|
{planIdx === 0 ? (plan.order_no || "-") : ""}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("due_date") && <TableCell className="text-center text-sm">
|
{ts.isVisible("due_date") && <TableCell style={ts.thStyle("due_date")} className="text-center text-sm">
|
||||||
{planIdx === 0 ? formatDate(plan.due_date) : ""}
|
{planIdx === 0 ? formatDate(plan.due_date) : ""}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("customer_name") && <TableCell className="text-sm">
|
{ts.isVisible("customer_name") && <TableCell style={ts.thStyle("customer_name")} className="text-sm">
|
||||||
{planIdx === 0 ? (plan.customer_name || "-") : ""}
|
{planIdx === 0 ? (plan.customer_name || "-") : ""}
|
||||||
</TableCell>}
|
</TableCell>}
|
||||||
{ts.isVisible("part_code") && <TableCell className="text-muted-foreground text-[13px]">{plan.part_code || "-"}</TableCell>}
|
{ts.isVisible("part_code") && <TableCell style={ts.thStyle("part_code")} className="text-muted-foreground text-[13px]">{plan.part_code || "-"}</TableCell>}
|
||||||
{ts.isVisible("part_name") && <TableCell className="font-medium text-sm">{plan.part_name || "-"}</TableCell>}
|
{ts.isVisible("part_name") && <TableCell style={ts.thStyle("part_name")} className="font-medium text-sm">{plan.part_name || "-"}</TableCell>}
|
||||||
{ts.isVisible("order_qty") && <TableCell className="text-right text-sm">{formatNumber(plan.order_qty)}</TableCell>}
|
{ts.isVisible("order_qty") && <TableCell style={ts.thStyle("order_qty")} className="text-right text-sm">{formatNumber(plan.order_qty)}</TableCell>}
|
||||||
{ts.isVisible("plan_qty") && <TableCell className="text-right font-semibold text-primary text-sm">{formatNumber(plan.plan_qty)}</TableCell>}
|
{ts.isVisible("plan_qty") && <TableCell style={ts.thStyle("plan_qty")} className="text-right font-semibold text-primary text-sm">{formatNumber(plan.plan_qty)}</TableCell>}
|
||||||
{ts.isVisible("plan_date") && <TableCell className="text-center text-sm">{formatDate(plan.plan_date)}</TableCell>}
|
{ts.isVisible("plan_date") && <TableCell style={ts.thStyle("plan_date")} className="text-center text-sm">{formatDate(plan.plan_date)}</TableCell>}
|
||||||
{ts.isVisible("status") && <TableCell className="text-center">
|
{ts.isVisible("status") && <TableCell style={ts.thStyle("status")} className="text-center">
|
||||||
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium", getStatusColor(plan.status))}>
|
<span className={cn("px-2 py-0.5 rounded-full text-[11px] font-medium", getStatusColor(plan.status))}>
|
||||||
{getStatusLabel(plan.status)}
|
{getStatusLabel(plan.status)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -57,12 +57,20 @@ export interface GroupSetting {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BaseFilter {
|
||||||
|
columnName: string;
|
||||||
|
operator: "equals" | "contains" | "in";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TableSettings {
|
export interface TableSettings {
|
||||||
columns: ColumnSetting[];
|
columns: ColumnSetting[];
|
||||||
filters: FilterSetting[];
|
filters: FilterSetting[];
|
||||||
groups: GroupSetting[];
|
groups: GroupSetting[];
|
||||||
frozenCount: number;
|
frozenCount: number;
|
||||||
groupSumEnabled: boolean;
|
groupSumEnabled: boolean;
|
||||||
|
/** 기본 데이터 필터 (예: division = '판매') */
|
||||||
|
baseFilter?: BaseFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TableSettingsModalProps {
|
export interface TableSettingsModalProps {
|
||||||
@@ -183,16 +191,17 @@ function SortableColumnRow({
|
|||||||
<div className="text-xs text-muted-foreground truncate">{col.columnName}</div>
|
<div className="text-xs text-muted-foreground truncate">{col.columnName}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 너비 입력 */}
|
{/* 너비 입력 (0 = 자동) */}
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
<span className="text-xs text-muted-foreground">너비:</span>
|
<span className="text-xs text-muted-foreground">너비:</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={col.width}
|
value={col.width || ""}
|
||||||
onChange={(e) => onWidthChange(col._idx, Number(e.target.value) || 100)}
|
onChange={(e) => onWidthChange(col._idx, Number(e.target.value) || 0)}
|
||||||
className="h-8 w-[70px] text-xs text-center"
|
className="h-8 w-[70px] text-xs text-center"
|
||||||
min={50}
|
min={0}
|
||||||
max={500}
|
max={500}
|
||||||
|
placeholder="자동"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,6 +229,8 @@ export function TableSettingsModal({
|
|||||||
const [tempGroups, setTempGroups] = useState<GroupSetting[]>([]);
|
const [tempGroups, setTempGroups] = useState<GroupSetting[]>([]);
|
||||||
const [tempFrozenCount, setTempFrozenCount] = useState(0);
|
const [tempFrozenCount, setTempFrozenCount] = useState(0);
|
||||||
const [tempGroupSum, setTempGroupSum] = useState(false);
|
const [tempGroupSum, setTempGroupSum] = useState(false);
|
||||||
|
const [tempBaseFilter, setTempBaseFilter] = useState<BaseFilter | undefined>();
|
||||||
|
const [baseFilterOptions, setBaseFilterOptions] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
|
||||||
// 원본 컬럼 (초기화용)
|
// 원본 컬럼 (초기화용)
|
||||||
const [defaultColumns, setDefaultColumns] = useState<ColumnSetting[]>([]);
|
const [defaultColumns, setDefaultColumns] = useState<ColumnSetting[]>([]);
|
||||||
@@ -248,7 +259,7 @@ export function TableSettingsModal({
|
|||||||
columnName: t.columnName,
|
columnName: t.columnName,
|
||||||
displayName: t.displayName || t.columnLabel || t.columnName,
|
displayName: t.displayName || t.columnLabel || t.columnName,
|
||||||
visible: defaultVisibleKeys ? defaultVisibleKeys.includes(t.columnName) : true,
|
visible: defaultVisibleKeys ? defaultVisibleKeys.includes(t.columnName) : true,
|
||||||
width: 120,
|
width: 0, // 0 = 자동 너비
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 활성 컬럼을 GRID_COLUMNS 순서대로 위에, 비활성을 아래에 정렬
|
// 활성 컬럼을 GRID_COLUMNS 순서대로 위에, 비활성을 아래에 정렬
|
||||||
@@ -313,12 +324,14 @@ export function TableSettingsModal({
|
|||||||
}));
|
}));
|
||||||
setTempFrozenCount(saved.frozenCount || 0);
|
setTempFrozenCount(saved.frozenCount || 0);
|
||||||
setTempGroupSum(saved.groupSumEnabled || false);
|
setTempGroupSum(saved.groupSumEnabled || false);
|
||||||
|
setTempBaseFilter(saved.baseFilter);
|
||||||
} else {
|
} else {
|
||||||
setTempColumns(freshColumns);
|
setTempColumns(freshColumns);
|
||||||
setTempFilters(freshFilters);
|
setTempFilters(freshFilters);
|
||||||
setTempGroups(freshGroups);
|
setTempGroups(freshGroups);
|
||||||
setTempFrozenCount(0);
|
setTempFrozenCount(0);
|
||||||
setTempGroupSum(false);
|
setTempGroupSum(false);
|
||||||
|
setTempBaseFilter(undefined);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("테이블 설정 로드 실패:", err);
|
console.error("테이블 설정 로드 실패:", err);
|
||||||
@@ -335,6 +348,7 @@ export function TableSettingsModal({
|
|||||||
groups: tempGroups,
|
groups: tempGroups,
|
||||||
frozenCount: tempFrozenCount,
|
frozenCount: tempFrozenCount,
|
||||||
groupSumEnabled: tempGroupSum,
|
groupSumEnabled: tempGroupSum,
|
||||||
|
baseFilter: tempBaseFilter,
|
||||||
};
|
};
|
||||||
localStorage.setItem(getStorageKey(settingsId), JSON.stringify(settings));
|
localStorage.setItem(getStorageKey(settingsId), JSON.stringify(settings));
|
||||||
onSave?.(settings);
|
onSave?.(settings);
|
||||||
@@ -502,32 +516,41 @@ export function TableSettingsModal({
|
|||||||
|
|
||||||
{/* ===== 탭 2: 필터 설정 ===== */}
|
{/* ===== 탭 2: 필터 설정 ===== */}
|
||||||
<TabsContent value="filters" className="mt-0 pt-3 overflow-y-auto max-h-[calc(80vh-220px)]">
|
<TabsContent value="filters" className="mt-0 pt-3 overflow-y-auto max-h-[calc(80vh-220px)]">
|
||||||
{/* 전체 선택 */}
|
{/* 검색 필터 설정 (상단) */}
|
||||||
|
<div className="flex items-center justify-between px-2 pb-2 border-b mb-2 shrink-0">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 px-2 pb-3 border-b mb-2 cursor-pointer"
|
className="flex items-center gap-2 cursor-pointer"
|
||||||
onClick={() => toggleFilterAll(!allFiltersEnabled)}
|
onClick={() => toggleFilterAll(!allFiltersEnabled)}
|
||||||
>
|
>
|
||||||
<Checkbox checked={allFiltersEnabled} />
|
<Checkbox checked={allFiltersEnabled} />
|
||||||
<span className="text-sm">전체 선택</span>
|
<span className="text-sm font-medium">검색 필터</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{tempFilters.filter((f) => f.enabled).length}/{tempFilters.length}개 활성
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필터 목록 */}
|
{/* 필터 목록 — 2열 그리드 */}
|
||||||
<div className="space-y-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
{tempFilters.map((filter, idx) => (
|
{tempFilters.map((filter, idx) => (
|
||||||
<div
|
<div
|
||||||
key={filter.columnName}
|
key={filter.columnName}
|
||||||
className="flex items-center gap-3 py-1.5 px-2 hover:bg-muted/50 rounded"
|
className={cn(
|
||||||
|
"flex items-center gap-2 py-1.5 px-2 rounded hover:bg-muted/50 cursor-pointer",
|
||||||
|
filter.enabled && "bg-primary/5",
|
||||||
|
)}
|
||||||
|
onClick={() => toggleFilter(idx)}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox checked={filter.enabled} className="shrink-0" />
|
||||||
checked={filter.enabled}
|
|
||||||
onCheckedChange={() => toggleFilter(idx)}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 text-sm min-w-0 truncate">{filter.displayName}</div>
|
<div className="flex-1 text-sm min-w-0 truncate">{filter.displayName}</div>
|
||||||
<Select
|
<Select
|
||||||
value={filter.filterType}
|
value={filter.filterType}
|
||||||
onValueChange={(v) => changeFilterType(idx, v as any)}
|
onValueChange={(v) => { changeFilterType(idx, v as any); }}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="h-7 w-[70px] text-[11px]"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-[90px] text-xs">
|
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -538,23 +561,99 @@ export function TableSettingsModal({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={filter.width}
|
|
||||||
onChange={(e) => changeFilterWidth(idx, Number(e.target.value) || 25)}
|
|
||||||
className="h-8 w-[55px] text-xs text-center"
|
|
||||||
min={10}
|
|
||||||
max={100}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-muted-foreground">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 기본 데이터 필터 (하단) */}
|
||||||
|
<div className="mt-4 rounded-lg border p-3 space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">기본 데이터 필터</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
이 화면에서 항상 적용되는 데이터 필터 조건
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tempBaseFilter ? (
|
||||||
|
<div className="flex items-center gap-2 rounded-md bg-primary/5 px-3 py-2">
|
||||||
|
<div className="flex-1 text-sm">
|
||||||
|
<span className="font-mono text-xs text-muted-foreground">{tableName}</span>
|
||||||
|
{" — "}
|
||||||
|
<span className="font-medium">{tempBaseFilter.columnName}</span>
|
||||||
|
{" = "}
|
||||||
|
<span className="text-primary font-medium">{tempBaseFilter.value || "(미설정)"}</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs text-destructive"
|
||||||
|
onClick={() => setTempBaseFilter(undefined)}
|
||||||
|
>
|
||||||
|
해제
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground px-1">필터 없음</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
value={tempBaseFilter?.columnName || ""}
|
||||||
|
onValueChange={async (col) => {
|
||||||
|
setTempBaseFilter({ columnName: col, operator: "equals", value: "" });
|
||||||
|
// 해당 컬럼의 카테고리 옵션 로드
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/table-categories/${tableName}/${col}/values`);
|
||||||
|
const vals = res.data?.data || [];
|
||||||
|
const flatten = (arr: any[]): { label: string; value: string }[] => {
|
||||||
|
const result: { label: string; value: string }[] = [];
|
||||||
|
for (const v of arr) {
|
||||||
|
result.push({ value: v.valueCode, label: v.valueLabel });
|
||||||
|
if (v.children?.length) result.push(...flatten(v.children));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
setBaseFilterOptions(flatten(vals));
|
||||||
|
} catch {
|
||||||
|
setBaseFilterOptions([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||||
|
<SelectValue placeholder="필터 컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{tempFilters.map((f) => (
|
||||||
|
<SelectItem key={f.columnName} value={f.columnName}>
|
||||||
|
{f.displayName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={tempBaseFilter?.value || ""}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
setTempBaseFilter((prev) => prev ? { ...prev, value: val } : prev)
|
||||||
|
}
|
||||||
|
disabled={!tempBaseFilter?.columnName}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||||
|
<SelectValue placeholder="필터 값 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{baseFilterOptions.map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 그룹별 합산 토글 */}
|
{/* 그룹별 합산 토글 */}
|
||||||
<div className="mt-4 flex items-center justify-between rounded-lg border p-3">
|
<div className="mt-3 flex items-center justify-between rounded-lg border p-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">그룹별 합산</div>
|
<div className="text-sm font-medium">그룹별 합산</div>
|
||||||
<div className="text-xs text-muted-foreground">같은 값끼리 그룹핑하여 합산</div>
|
<div className="text-xs text-muted-foreground">같은 값끼리 그룹핑하여 합산</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
|||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground z-[2000] w-72 rounded-md border p-4 shadow-md outline-none",
|
"bg-popover text-popover-foreground z-[10002] w-72 rounded-md border p-4 shadow-md outline-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
* />
|
* />
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { loadTableSettings, type TableSettings } from "@/components/common/TableSettingsModal";
|
import { loadTableSettings, type TableSettings, type BaseFilter } from "@/components/common/TableSettingsModal";
|
||||||
|
|
||||||
export function useTableSettings<T extends { key: string }>(
|
export function useTableSettings<T extends { key: string }>(
|
||||||
settingsId: string,
|
settingsId: string,
|
||||||
@@ -45,6 +45,8 @@ export function useTableSettings<T extends { key: string }>(
|
|||||||
const [orderedKeys, setOrderedKeys] = useState<string[]>(
|
const [orderedKeys, setOrderedKeys] = useState<string[]>(
|
||||||
() => initialVisibleKeys || defaultColumns.map((c) => c.key),
|
() => initialVisibleKeys || defaultColumns.map((c) => c.key),
|
||||||
);
|
);
|
||||||
|
const [baseFilter, setBaseFilter] = useState<BaseFilter | undefined>();
|
||||||
|
|
||||||
// 초기 filterConfig: GRID_COLUMNS에 있는 컬럼만 필터 가능 (전부 비활성)
|
// 초기 filterConfig: GRID_COLUMNS에 있는 컬럼만 필터 가능 (전부 비활성)
|
||||||
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"]>(
|
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"]>(
|
||||||
() =>
|
() =>
|
||||||
@@ -91,6 +93,9 @@ export function useTableSettings<T extends { key: string }>(
|
|||||||
setFilterConfig(
|
setFilterConfig(
|
||||||
settings.filters?.filter((f) => visible.has(f.columnName)),
|
settings.filters?.filter((f) => visible.has(f.columnName)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 기본 데이터 필터
|
||||||
|
setBaseFilter(settings.baseFilter);
|
||||||
},
|
},
|
||||||
[defaultColumns, initialVisibleKeys],
|
[defaultColumns, initialVisibleKeys],
|
||||||
);
|
);
|
||||||
@@ -133,6 +138,16 @@ export function useTableSettings<T extends { key: string }>(
|
|||||||
[columnWidths],
|
[columnWidths],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** TableHead/TableCell에 적용할 style 객체 (0 = 자동, 값 있으면 고정) */
|
||||||
|
const thStyle = useCallback(
|
||||||
|
(key: string): React.CSSProperties | undefined => {
|
||||||
|
const w = columnWidths[key];
|
||||||
|
if (!w || w <= 0) return undefined; // 0이면 브라우저 자동
|
||||||
|
return { width: `${w}px`, minWidth: `${w}px`, maxWidth: `${w}px` };
|
||||||
|
},
|
||||||
|
[columnWidths],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/** 모달 open 상태 */
|
/** 모달 open 상태 */
|
||||||
open,
|
open,
|
||||||
@@ -150,8 +165,12 @@ export function useTableSettings<T extends { key: string }>(
|
|||||||
isVisible,
|
isVisible,
|
||||||
/** 특정 컬럼 너비 (px) */
|
/** 특정 컬럼 너비 (px) */
|
||||||
getWidth,
|
getWidth,
|
||||||
|
/** TableHead/TableCell style 객체 반환 */
|
||||||
|
thStyle,
|
||||||
/** 필터 설정 */
|
/** 필터 설정 */
|
||||||
filterConfig,
|
filterConfig,
|
||||||
|
/** 기본 데이터 필터 (예: division = '판매') */
|
||||||
|
baseFilter,
|
||||||
/** GRID_COLUMNS 기본 컬럼 키 목록 (TableSettingsModal defaultVisibleKeys용) */
|
/** GRID_COLUMNS 기본 컬럼 키 목록 (TableSettingsModal defaultVisibleKeys용) */
|
||||||
defaultVisibleKeys: initialVisibleKeys || defaultColumns.map((c) => c.key),
|
defaultVisibleKeys: initialVisibleKeys || defaultColumns.map((c) => c.key),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user