diff --git a/_pipeline/reviews/plan-review.md b/_pipeline/reviews/plan-review.md new file mode 100644 index 00000000..52e7b86f --- /dev/null +++ b/_pipeline/reviews/plan-review.md @@ -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 전체 명세 포함 필요 \ No newline at end of file diff --git a/docs/coding-rules/pipeline-backend.md b/docs/coding-rules/pipeline-backend.md new file mode 100644 index 00000000..6b4ff99c --- /dev/null +++ b/docs/coding-rules/pipeline-backend.md @@ -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 diff --git a/docs/coding-rules/pipeline-common-rules.md b/docs/coding-rules/pipeline-common-rules.md new file mode 100644 index 00000000..ba7fd7ec --- /dev/null +++ b/docs/coding-rules/pipeline-common-rules.md @@ -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 `로 프로세스 종료 +- `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 상태 요약 diff --git a/docs/coding-rules/pipeline-db.md b/docs/coding-rules/pipeline-db.md new file mode 100644 index 00000000..33e25218 --- /dev/null +++ b/docs/coding-rules/pipeline-db.md @@ -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 diff --git a/docs/coding-rules/pipeline-frontend.md b/docs/coding-rules/pipeline-frontend.md new file mode 100644 index 00000000..0eef5611 --- /dev/null +++ b/docs/coding-rules/pipeline-frontend.md @@ -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 diff --git a/docs/coding-rules/pipeline-ui.md b/docs/coding-rules/pipeline-ui.md new file mode 100644 index 00000000..dc6c1c49 --- /dev/null +++ b/docs/coding-rules/pipeline-ui.md @@ -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 +
+ +

섹션 제목

+ {count} +
+``` + +### 상태 표시 카드: 설정됨/안됨 시각화 +- 활성: `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 diff --git a/docs/coding-rules/pipeline-verifier.md b/docs/coding-rules/pipeline-verifier.md new file mode 100644 index 00000000..a4f4186d --- /dev/null +++ b/docs/coding-rules/pipeline-verifier.md @@ -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. diff --git a/docs/coding-rules/presets/erp-preset-common.css b/docs/coding-rules/presets/erp-preset-common.css new file mode 100644 index 00000000..82f6da0c --- /dev/null +++ b/docs/coding-rules/presets/erp-preset-common.css @@ -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; } diff --git a/docs/coding-rules/presets/erp-preset-type-a-single-table.html b/docs/coding-rules/presets/erp-preset-type-a-single-table.html new file mode 100644 index 00000000..cd3276ce --- /dev/null +++ b/docs/coding-rules/presets/erp-preset-type-a-single-table.html @@ -0,0 +1,1163 @@ + + + + + + ERP Preset - Type A: 단일 테이블형 + + + + + +
+ + + 라이트 +
+ +
+ + + + +
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ + ~ + +
+
+
+ +
+
확정 ×
+
진행중 ×
+
+
+
+ + +
+
+
+ + +
+
+

수주 목록

+ 128건 +
+
+ + + +
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
수주번호품번품명규격수량단가금액납기일상태
+
+ + +
+
+
+ 총 128건 중 1-10 표시 +
+
+ 페이지당 +
+ +
+
+
+
+ + + + + + ... + + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/docs/coding-rules/presets/erp-preset-type-b-master-detail.html b/docs/coding-rules/presets/erp-preset-type-b-master-detail.html new file mode 100644 index 00000000..9ca91da2 --- /dev/null +++ b/docs/coding-rules/presets/erp-preset-type-b-master-detail.html @@ -0,0 +1,1116 @@ + + + + + +ERP Type B: 마스터-디테일형 (거래처관리) + + + + +
+ + +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+ 거래처 목록 + 8건 +
+
+ + +
+
+
+ + + + + + + + + + + + + + + +
거래처코드거래처명대표자연락처유형상태
+
+
+ + +
+ + +
+ +
+ + + + + +
거래처를 선택해주세요
+
좌측에서 거래처를 선택하면 상세 정보가 표시돼요
+
+ + + +
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/docs/coding-rules/presets/erp-preset-type-c-tree-detail.html b/docs/coding-rules/presets/erp-preset-type-c-tree-detail.html new file mode 100644 index 00000000..75325f60 --- /dev/null +++ b/docs/coding-rules/presets/erp-preset-type-c-tree-detail.html @@ -0,0 +1,1309 @@ + + + + + +ERP Preset Type C: Tree + Detail - BOM 관리 + + + + + + +
+ +
+
+ Theme +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ + +
+
+
+ + BOM 목록 + 6건 +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BOM코드품목코드품명버전유효시작일상태
BOM-2026-001FP-A1000스마트 센서 모듈 v31.02026-01-15활성
BOM-2026-002FP-B2000전력 변환 장치 MKII2.12026-02-01활성
BOM-2026-003FP-C3000통신 제어 보드 R41.22026-03-10초안
BOM-2025-018FP-D4000냉각 어셈블리 Pro3.02025-06-01만료
BOM-2026-004FP-E5000디스플레이 패널 UHD1.02026-03-20초안
BOM-2025-012FP-F6000배터리 팩 60kWh2.02025-09-01비활성
+
+
+ + +
+ + +
+ + +
+
+
+ + BOM 상세정보 +
+ +
+
+
+ 품목코드 +
FP-A1000
+
+
+ 품명 +
스마트 센서 모듈 v3
+
+
+ 규격 +
120x80x25mm / IP67
+
+
+ BOM 유형 +
생산 BOM
+
+
+ 버전 +
1.0
+
+
+ 상태 +
활성
+
+
+ 메모 +
2026년 1차 양산 기준 BOM. 센서 모듈 v3 설계 확정본.
+
+
+
+ + +
+
+
+ + BOM 트리뷰 +
+
+
+ + +
+
+
+ + +
+
+
+ +
+ +
+
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/docs/coding-rules/presets/erp-preset-type-d-tab-multiview.html b/docs/coding-rules/presets/erp-preset-type-d-tab-multiview.html new file mode 100644 index 00000000..178e15df --- /dev/null +++ b/docs/coding-rules/presets/erp-preset-type-d-tab-multiview.html @@ -0,0 +1,1274 @@ + + + + + + ERP Preset - Type D: 탭 멀티뷰형 + + + + + +
+ + + 라이트 +
+ +
+ + + + +
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+
+ + +
+ + + +
+ + +
+
+
+

검사기준 목록

+ 12건 +
+
+ + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
검사코드검사명검사유형검사항목수적용품목수상태등록일
QI-2026-0012PCB 외관검사 기준수입검사815사용2026-03-28
QI-2026-0011전자부품 전기특성 검사공정검사128사용2026-03-25
QI-2026-0010완제품 기능시험 기준완제품검사153사용2026-03-20
QI-2026-0009출하 포장상태 검사출하검사622대기2026-03-18
QI-2026-0008원자재 치수검사 기준수입검사510미사용2026-03-15
+
+
+
+
+ 총 12건 중 1-5 표시 +
+
+ 페이지당 +
+ +
+
+
+
+ + + + +
+
+
+
+ + +
+
+
+

불량유형 목록

+ 8건 +
+
+ + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
불량코드불량유형불량명심각도설명상태
DF-001외관불량스크래치표면 긁힘, 0.5mm 이상 시 불량 판정사용
DF-002치수불량규격초과허용 공차 범위 초과사용
DF-003기능불량동작불량정상 동작 불가, 전수검사 대상사용
DF-004외관불량변색색상 차이 발생, 경미한 수준사용
DF-005포장불량파손포장재 손상으로 인한 제품 파손 우려미사용
+
+
+
+
+ 총 8건 중 1-5 표시 +
+
+ 페이지당 +
+ +
+
+
+
+ + + + +
+
+
+
+ + +
+
+
+

검사장비 목록

+ 5건 +
+
+ + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
장비코드장비명모델명제조사교정주기최종교정일상태
EQ-0013D 측정기CRYSTA-Apex V미츠토요12개월2026-01-15정상
EQ-002경도시험기HR-530L미츠토요6개월2025-12-20교정예정
EQ-003디지털 현미경VHX-7000키엔스12개월2026-02-10정상
+
+
+
+
+ 총 5건 중 1-3 표시 +
+
+ 페이지당 +
+ +
+
+
+
+ + + +
+
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/coding-rules/presets/erp-preset-type-e-card-list.html b/docs/coding-rules/presets/erp-preset-type-e-card-list.html new file mode 100644 index 00000000..bfee93e2 --- /dev/null +++ b/docs/coding-rules/presets/erp-preset-type-e-card-list.html @@ -0,0 +1,1345 @@ + + + + + + ERP Preset - Type E: 카드 리스트형 + + + + + +
+ + + 라이트 +
+ +
+ + + + +
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+ + +
+ + +
+
+
+

금형 목록

+ 5 +
+
+
+ + +
+ +
+
+ +
+
+ +
+
+
+ + +
+ + +
+
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/coding-rules/presets/erp-preset-type-f-report.html b/docs/coding-rules/presets/erp-preset-type-f-report.html new file mode 100644 index 00000000..fb0aadd4 --- /dev/null +++ b/docs/coding-rules/presets/erp-preset-type-f-report.html @@ -0,0 +1,1272 @@ + + + + + +ERP Preset - Type F: 리포트/분석형 + + + + + + +
+
+ +
+
+
+ + 다크 +
+
+
+ +
+ + +
+
+

+ + 영업리포트 +

+
+
+
+
+ 자동갱신 (30초) +
+
+
+ + +
+
+ + +
+ + + 프리셋 + + + + + + + +
+ + +
+ +
+
+ +
+ + + + +
+
+
+ +
+ + ~ + +
+
+
+ +
+ + + + + + +
+
+
+ + +
+
+ +
+ (주)삼성전자 x + LG화학 x + +3 +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + 총 매출 +
+
₩1,234,567,890
+ +
+ +
+
+ + 수주 건수 +
+
156
+ +
+ +
+
+ + 출하 완료율 +
+
94.2%
+ +
+
+
94%
+
+
+
+ +
+
+ + 평균 납기 준수율 +
+
87.6%
+ +
+
+
88%
+
+
+
+
+ + +
+ +
+
+ + + 월별 매출 추이 + + 2025.10 ~ 2026.03 (단위: 억원) +
+
+
+ 임계값 5억 +
+
+
3.6억
+ 10월 +
+
+
4.4억
+ 11월 +
+
+
5.8억
+ 12월 +
+
+
3.0억
+ 1월 +
+
+
5.2억
+ 2월 +
+
+
7.0억
+ 3월 +
+
+
+ + +
+
+ + + 거래처별 매출 비중 + + 전체 12.3억 기준 +
+
+
+
+ 합계 + 12.3억 +
+
+
+
+ + (주)삼성전자 + 4.3억 + 35% +
+
+ + LG화학 + 3.1억 + 25% +
+
+ + SK하이닉스 + 2.2억 + 18% +
+
+ + 현대모비스 + 1.5억 + 12% +
+
+ + 기타 + 1.2억 + 10% +
+
+
+
+
+ + +
+
+

+ + 월별 집계 테이블 +

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#기준수주건수수주금액출하건수출하금액매출전기대비(%)
+ + 2025년 4분기 +
12025년 10월24385,200,00022362,100,000360,000,000+8.3%
22025년 11월28462,800,00026448,500,000440,000,000+22.2%
32025년 12월32612,400,00030590,200,000580,000,000+31.8%
Q4 소계841,460,400,000781,400,800,0001,380,000,000+20.6%
+ + 2026년 1분기 +
42026년 1월18312,600,00016298,400,000300,000,000-16.7%
52026년 2월26538,200,00024520,800,000520,000,000+73.3%
62026년 3월28725,800,00027708,300,000700,000,000+34.6%
Q1 소계721,576,600,000671,527,500,0001,520,000,000+10.1%
합계1563,037,000,0001452,928,300,0002,900,000,000+15.2%
+
+
+ + +
+
+

+ + 드릴다운 상세 데이터 + - 2026년 3월 +

+ +
+
+
+ + 집계 테이블의 행을 클릭하면 해당 기간의 상세 거래 내역이 여기에 표시돼요. +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#수주일수주번호거래처품목담당자수주금액출하금액상태납기일
12026-03-02SO-20260302-001(주)삼성전자반도체 웨이퍼 A1김영업185,400,000185,400,000출하완료2026-03-15
22026-03-05SO-20260305-002LG화학화학소재 B2박대리92,300,00092,300,000출하완료2026-03-20
32026-03-08SO-20260308-003SK하이닉스메모리 모듈 C3이과장156,800,000156,800,000출하완료2026-03-22
42026-03-12SO-20260312-004현대모비스차량 센서 D4김영업128,500,000128,500,000출하중2026-03-28
52026-03-15SO-20260315-005(주)삼성전자디스플레이 패널 E5박대리98,200,0000수주확정2026-04-05
62026-03-18SO-20260318-006LG화학특수 코팅재 F6이과장64,600,00064,600,000출하완료2026-03-30
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/docs/coding-rules/ui-design-philosophy.mdc b/docs/coding-rules/ui-design-philosophy.mdc new file mode 100644 index 00000000..13c71b73 --- /dev/null +++ b/docs/coding-rules/ui-design-philosophy.mdc @@ -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 +// 금지: 텍스트가 밀리는 카드 + + +// 필수: 정렬된 카드 + +``` + +### 2. 입력 폭 불일치 금지 +같은 영역에 있는 Input, Select 등 폼 컨트롤은 반드시 동일한 폭을 가져야 한다. +- 같은 섹션의 Input이 서로 다른 너비를 가지면 안 된다 +- 나란히 배치된 필드(너비/높이 등)는 정확히 같은 폭이어야 한다 + +```tsx +// 금지: 폭이 다른 입력 필드 + // Z-Index + // 높이 <- 폭이 다름! + +// 필수: 폭 일관성 +
+
+
+
+
+ + // 같은 w-full +
+``` + +### 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 +
+
+ +

필드 매핑

+ + {mappedCount}/{totalCount} + +
+

+ 상위 폼의 필드 중 렉 생성에 사용할 필드를 선택해요 +

+
+``` + +**규칙:** +- 아이콘: `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 +
+ {items.map((item) => { + const isActive = !!item.value; + return ( +
+ {isActive + ? + : } +
+

{item.label}

+

{item.description}

+
+ +
+ ); + })} +
+``` + +**규칙:** +- 활성 상태: `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 +
+
+

최대 조건

+ +
+
+

최대 열

+ +
+
+

최대 단

+ +
+
+``` + +**규칙:** +- 입력 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 +
+

이 필드는 어떤 데이터를 선택하나요?

+
+ {cards.map(card => ( + + ))} +
+
+``` + +**카드 필수 규칙:** +- 모든 카드는 동일한 높이 (`min-h-[80px]`) +- 텍스트는 center 정렬 +- 아이콘은 텍스트 위에 +- 설명은 `text-[10px] text-muted-foreground` +- 선택된 카드: `border-primary bg-primary/5 ring-1 ring-primary/20` + +### 고급 설정 패턴 (Progressive Disclosure) +자주 안 쓰는 설정은 Collapsible로 숨긴다. 기본은 접혀있다. + +```tsx + + + + + +
+ {/* 고급 옵션들 */} +
+
+
+``` + +**Collapsible 규칙:** +- 펼쳐진 콘텐츠에 `max-h`나 `overflow-y-auto` 절대 금지 (펼치면 전부 보여야 함) +- 접혀있을 때 Badge로 현재 상태 요약 (예: "3개 설정됨") +- ChevronDown에 `[[data-state=open]>&]:rotate-180` 트랜지션 + +### 토글 옵션 패턴 (Switch + 설명) +각 토글 옵션에 제목과 설명을 함께 보여준다. 사용자가 뭘 켜는 건지 이해할 수 있다. + +```tsx +
+
+

여러 개 선택

+

한 번에 여러 값을 선택할 수 있어요

+
+ +
+``` + +**규칙:** +- 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 +
+
+ + {title} +
+ {/* 소스별 설정 내용 */} +
+``` + +### 빈 상태 패턴 +데이터가 없을 때 친절하게 안내한다. + +```tsx +
+ +

아직 옵션이 없어요

+

위의 추가 버튼으로 옵션을 만들어보세요

+
+``` + +### Property Row 패턴 (라벨 + 컨트롤) +간단한 설정은 수평 배치한다. + +```tsx +
+ 기본 선택값 + +
+``` + +--- + +## 스크롤 제한 정책 (max-h / overflow) + +### 인라인 콘텐츠에 max-h 금지 (절대 규칙!) + +Collapsible이 펼쳐졌을 때 콘텐츠가 스크롤에 잘려서 보이지 않으면 안 된다. +"펼치기"의 의미는 전부 보여주는 것이다. 스크롤을 걸면 눈가리기일 뿐이다. + +```tsx +// 금지: 펼친 콘텐츠에 스크롤 제한 + +
+ +// 허용: 드롭다운/팝업 오버레이에만 max-h 사용 + + +``` + +**허용 대상** (팝업 오버레이): +- `CommandGroup`, `CommandList` (Combobox 드롭다운) +- `SelectContent` (Select 드롭다운) +- `PopoverContent` 내부 리스트 + +**금지 대상** (인라인 콘텐츠): +- `CollapsibleContent` 내부 +- 일반 `div` 안의 리스트 +- 설정 패널 본문의 아이템 목록 + +**대안**: Collapsible 접기/펴기로 영역을 줄이되, 펼쳤을 때는 전체를 보여준다. + +--- + +## 텍스트 오버플로우 처리 (필수!) + +모든 동적 텍스트(필드명, 테이블명, 컬럼명 등)에 overflow 방지 처리가 필수. + +```tsx +// 필수: 모든 동적 텍스트에 truncate +

{fieldName}

+ +// 필수: flex 안의 텍스트 블록에 min-w-0 +
+
+

{longText}

+
+ {/* 고정 폭 컨트롤 */} +
+ +// 필수: Switch/Select 옆의 설명 블록 +
+

라벨

+

설명

+
+``` + +**핵심 규칙:** +- `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`이 있는가? diff --git a/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx b/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx index 5ceedc05..00fedd3f 100644 --- a/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/change-management/page.tsx @@ -866,7 +866,7 @@ export default function DesignChangeManagementPage() {
{currentTab === "ecr" ? ( - +
No @@ -913,23 +913,23 @@ export default function DesignChangeManagementPage() { onClick={() => handleRowClick(item.id)} > {idx + 1} - {tsEcr.isVisible("request_no") && {item.id}} + {tsEcr.isVisible("request_no") && {item.id}} {tsEcr.isVisible("change_type") && ( - + {item.changeType} )} {tsEcr.isVisible("status") && ( - + {item.status} )} {tsEcr.isVisible("urgency") && ( - + {item.urgency === "긴급" ? ( 긴급 @@ -939,13 +939,13 @@ export default function DesignChangeManagementPage() { )} )} - {tsEcr.isVisible("target_name") && {item.target}} - {tsEcr.isVisible("drawing_no") && {item.drawingNo}} - {tsEcr.isVisible("req_dept") && {item.reqDept}} - {tsEcr.isVisible("requester") && {item.requester}} - {tsEcr.isVisible("request_date") && {item.date}} + {tsEcr.isVisible("target_name") && {item.target}} + {tsEcr.isVisible("drawing_no") && {item.drawingNo}} + {tsEcr.isVisible("req_dept") && {item.reqDept}} + {tsEcr.isVisible("requester") && {item.requester}} + {tsEcr.isVisible("request_date") && {item.date}} {tsEcr.isVisible("ecn_no") && ( - + {item.ecnNo ? (
) : ( - +
No @@ -1013,22 +1013,22 @@ export default function DesignChangeManagementPage() { onClick={() => handleRowClick(item.id)} > {idx + 1} - {tsEcn.isVisible("ecn_no") && {item.id}} + {tsEcn.isVisible("ecn_no") && {item.id}} {tsEcn.isVisible("status") && ( - + {item.status} )} - {tsEcn.isVisible("target") && {item.target}} - {tsEcn.isVisible("drawing_after") && {item.drawingAfter}} - {tsEcn.isVisible("designer") && {item.designer}} - {tsEcn.isVisible("ecn_date") && {item.date}} - {tsEcn.isVisible("apply_date") && {item.applyDate}} - {tsEcn.isVisible("notify_depts") && {item.notifyDepts.join(", ")}} + {tsEcn.isVisible("target") && {item.target}} + {tsEcn.isVisible("drawing_after") && {item.drawingAfter}} + {tsEcn.isVisible("designer") && {item.designer}} + {tsEcn.isVisible("ecn_date") && {item.date}} + {tsEcn.isVisible("apply_date") && {item.applyDate}} + {tsEcn.isVisible("notify_depts") && {item.notifyDepts.join(", ")}} {tsEcn.isVisible("ecr_id") && ( - +
+
{ts.visibleColumns.map((col) => ( @@ -484,7 +484,7 @@ export default function DesignRequestPage() { col.key === "due_date" && "w-[85px]", 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} @@ -510,30 +510,30 @@ export default function DesignRequestPage() { className={cn("cursor-pointer", selectedId === item.id && "bg-accent")} onClick={() => handleRowClick(item.id)} > - {ts.isVisible("request_no") && {item.request_no || "-"}} + {ts.isVisible("request_no") && {item.request_no || "-"}} {ts.isVisible("design_type") && ( - + {item.design_type ? ( {item.design_type} ) : "-"} )} {ts.isVisible("status") && ( - + {item.status} )} {ts.isVisible("priority") && ( - + {item.priority} )} - {ts.isVisible("target_name") && {item.target_name || "-"}} - {ts.isVisible("customer") && {item.customer || "-"}} - {ts.isVisible("designer") && {item.designer || "-"}} - {ts.isVisible("due_date") && {item.due_date || "-"}} + {ts.isVisible("target_name") && {item.target_name || "-"}} + {ts.isVisible("customer") && {item.customer || "-"}} + {ts.isVisible("designer") && {item.designer || "-"}} + {ts.isVisible("due_date") && {item.due_date || "-"}} {ts.isVisible("progress") && ( - +
diff --git a/frontend/app/(main)/COMPANY_16/design/project/page.tsx b/frontend/app/(main)/COMPANY_16/design/project/page.tsx index 89d06ae8..1aaafc9f 100644 --- a/frontend/app/(main)/COMPANY_16/design/project/page.tsx +++ b/frontend/app/(main)/COMPANY_16/design/project/page.tsx @@ -728,7 +728,7 @@ export default function DesignProjectPage() {
-
+
{ts.visibleColumns.map((col) => ( @@ -746,7 +746,7 @@ export default function DesignProjectPage() { col.key === "progress" && "w-[100px] text-center", col.key === "source_no" && "w-[90px]", )} - style={ts.getWidth(col.key) ? { width: ts.getWidth(col.key) } : undefined} + style={ts.thStyle(col.key)} > {col.label} @@ -794,7 +794,7 @@ export default function DesignProjectPage() { }} > {ts.isVisible("project_no") && ( - +
{hasChildren ? (
-
+
{ts.visibleColumns.map((col) => ( @@ -769,7 +769,7 @@ export default function DesignTaskManagementPage() { col.key === "due_date" && "w-[100px]", col.key === "designer" && "w-[80px]", )} - style={ts.getWidth(col.key) ? { width: ts.getWidth(col.key) } : undefined} + style={ts.thStyle(col.key)} > {col.label} @@ -810,38 +810,38 @@ export default function DesignTaskManagementPage() { onClick={() => handleSelectTask(item.dbId)} > {ts.isVisible("source_type") && ( - + {item.sourceType === "dr" ? "DR" : "ECR"} )} {ts.isVisible("request_no") && ( - + {item.id} )} {ts.isVisible("status") && ( - + {item.status} )} {ts.isVisible("priority") && ( - + {item.priority} )} - {ts.isVisible("target_name") && {item.targetName}} - {ts.isVisible("req_dept") && {item.reqDept}} - {ts.isVisible("requester") && {item.requester}} - {ts.isVisible("request_date") && {item.date}} - {ts.isVisible("due_date") && {item.dueDate}} + {ts.isVisible("target_name") && {item.targetName}} + {ts.isVisible("req_dept") && {item.reqDept}} + {ts.isVisible("requester") && {item.requester}} + {ts.isVisible("request_date") && {item.date}} + {ts.isVisible("due_date") && {item.dueDate}} {ts.isVisible("designer") && ( - + {item.designer || 미배정} )} diff --git a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx index 88034b88..314db047 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx @@ -442,15 +442,15 @@ export default function EquipmentInfoPage() {

등록된 설비가 없어요

) : ( -
+
- {ts.isVisible("equipment_code") && 설비코드} - {ts.isVisible("equipment_name") && 설비명} - {ts.isVisible("equipment_type") && 설비유형} - {ts.isVisible("manufacturer") && 제조사} - {ts.isVisible("installation_location") && 설치장소} - {ts.isVisible("operation_status") && 가동상태} + {ts.isVisible("equipment_code") && 설비코드} + {ts.isVisible("equipment_name") && 설비명} + {ts.isVisible("equipment_type") && 설비유형} + {ts.isVisible("manufacturer") && 제조사} + {ts.isVisible("installation_location") && 설치장소} + {ts.isVisible("operation_status") && 가동상태} @@ -461,12 +461,12 @@ export default function EquipmentInfoPage() { onClick={() => setSelectedEquipId(equip.id)} onDoubleClick={openEquipEdit} > - {ts.isVisible("equipment_code") && {equip.equipment_code}} - {ts.isVisible("equipment_name") && {equip.equipment_name || "-"}} - {ts.isVisible("equipment_type") && {equip.equipment_type || "-"}} - {ts.isVisible("manufacturer") && {equip.manufacturer || "-"}} - {ts.isVisible("installation_location") && {equip.installation_location || "-"}} - {ts.isVisible("operation_status") && {equip.operation_status || "-"}} + {ts.isVisible("equipment_code") && {equip.equipment_code}} + {ts.isVisible("equipment_name") && {equip.equipment_name || "-"}} + {ts.isVisible("equipment_type") && {equip.equipment_type || "-"}} + {ts.isVisible("manufacturer") && {equip.manufacturer || "-"}} + {ts.isVisible("installation_location") && {equip.installation_location || "-"}} + {ts.isVisible("operation_status") && {equip.operation_status || "-"}} ))} diff --git a/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx index 36fcf00a..5a3cd862 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/plc-settings/page.tsx @@ -285,7 +285,7 @@ export default function PlcSettingsPage() {
-
+
@@ -295,7 +295,7 @@ export default function PlcSettingsPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -315,7 +315,7 @@ export default function PlcSettingsPage() { setDtChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> {ts.visibleColumns.map((col) => ( - + {col.key === "is_active" ? {row.is_active ? "사용" : "미사용"} : row[col.key] ?? ""} diff --git a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx index 5bbb5a7a..ec0c401d 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx @@ -764,7 +764,7 @@ export default function LogisticsInfoPage() { ) : ( -
+
@@ -783,11 +783,7 @@ export default function LogisticsInfoPage() { col.align === "right" && "text-right", col.align === "center" && "text-center" )} - style={ - col.width - ? { width: col.width, minWidth: col.width } - : undefined - } + style={tsMap[tab.key].thStyle(col.key)} > {col.label} @@ -829,6 +825,7 @@ export default function LogisticsInfoPage() { col.align === "right" && "text-right", col.align === "center" && "text-center" )} + style={tsMap[tab.key].thStyle(col.key)} > {display} diff --git a/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx index 940dc89d..ebbd812e 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/inventory/page.tsx @@ -378,13 +378,14 @@ export default function InventoryStatusPage() { 등록된 재고가 없어요 ) : ( -
+
# {ts.visibleColumns.map((col) => ( {col.label} @@ -409,7 +410,7 @@ export default function InventoryStatusPage() { {ts.visibleColumns.map((col) => { if (col.key === "current_qty") { return ( - + {Number(item.current_qty || 0).toLocaleString()} @@ -421,14 +422,14 @@ export default function InventoryStatusPage() { } if (col.key === "safety_qty") { return ( - + {Number(item.safety_qty || 0).toLocaleString()} ); } if (col.key === "status") { return ( - + {item.status} @@ -436,7 +437,7 @@ export default function InventoryStatusPage() { ); } return ( - + {item[col.key] ?? ""} ); diff --git a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx index df51c4d6..f9422f27 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/outbound/page.tsx @@ -536,7 +536,7 @@ export default function OutboundPage() {
-
+
@@ -545,21 +545,21 @@ export default function OutboundPage() { onCheckedChange={toggleCheckAll} /> - {ts.isVisible("outbound_number") && 출고번호} - {ts.isVisible("outbound_type") && 출고유형} - {ts.isVisible("outbound_date") && 출고일} - {ts.isVisible("reference_number") && 참조번호} - {ts.isVisible("source_type") && 데이터출처} - {ts.isVisible("customer_name") && 거래처} - {ts.isVisible("item_number") && 품목코드} - {ts.isVisible("item_name") && 품목명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("outbound_qty") && 출고수량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("total_amount") && 금액} - {ts.isVisible("warehouse_name") && 창고} - {ts.isVisible("outbound_status") && 출고상태} - {ts.isVisible("remark") && 비고} + {ts.isVisible("outbound_number") && 출고번호} + {ts.isVisible("outbound_type") && 출고유형} + {ts.isVisible("outbound_date") && 출고일} + {ts.isVisible("reference_number") && 참조번호} + {ts.isVisible("source_type") && 데이터출처} + {ts.isVisible("customer_name") && 거래처} + {ts.isVisible("item_number") && 품목코드} + {ts.isVisible("item_name") && 품목명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("outbound_qty") && 출고수량} + {ts.isVisible("unit_price") && 단가} + {ts.isVisible("total_amount") && 금액} + {ts.isVisible("warehouse_name") && 창고} + {ts.isVisible("outbound_status") && 출고상태} + {ts.isVisible("remark") && 비고} @@ -603,10 +603,10 @@ export default function OutboundPage() { onCheckedChange={() => toggleCheck(row.id)} /> - {ts.isVisible("outbound_number") && + {ts.isVisible("outbound_number") && {row.outbound_number} } - {ts.isVisible("outbound_type") && + {ts.isVisible("outbound_type") && } - {ts.isVisible("outbound_date") && + {ts.isVisible("outbound_date") && {row.outbound_date ? new Date(row.outbound_date).toLocaleDateString("ko-KR") : "-"} } - {ts.isVisible("reference_number") && + {ts.isVisible("reference_number") && {row.reference_number || "-"} } - {ts.isVisible("source_type") && + {ts.isVisible("source_type") && {row.source_type ? SOURCE_TYPE_LABEL[row.source_type] || row.source_type : "-"} } - {ts.isVisible("customer_name") && + {ts.isVisible("customer_name") && {row.customer_name || "-"} } - {ts.isVisible("item_number") && + {ts.isVisible("item_number") && {row.item_code || "-"} } - {ts.isVisible("item_name") && {row.item_name || "-"}} - {ts.isVisible("spec") && {row.specification || "-"}} - {ts.isVisible("outbound_qty") && + {ts.isVisible("item_name") && {row.item_name || "-"}} + {ts.isVisible("spec") && {row.specification || "-"}} + {ts.isVisible("outbound_qty") && {Number(row.outbound_qty || 0).toLocaleString()} } - {ts.isVisible("unit_price") && + {ts.isVisible("unit_price") && {Number(row.unit_price || 0).toLocaleString()} } - {ts.isVisible("total_amount") && + {ts.isVisible("total_amount") && {Number(row.total_amount || 0).toLocaleString()} } - {ts.isVisible("warehouse_name") && + {ts.isVisible("warehouse_name") && {row.warehouse_name || row.warehouse_code || "-"} } - {ts.isVisible("outbound_status") && + {ts.isVisible("outbound_status") && } - {ts.isVisible("remark") && + {ts.isVisible("remark") && {row.memo || "-"} } diff --git a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx index f97b8f1f..1a04aa7f 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx @@ -458,15 +458,15 @@ export default function PackagingPage() {
{/* 포장재 목록 테이블 */}
-
+
- {ts.isVisible("pkg_code") && 품목코드} - {ts.isVisible("pkg_name") && 포장명} - {ts.isVisible("pkg_type") && 유형} - {ts.isVisible("size") && 크기(mm)} - {ts.isVisible("max_weight") && 최대중량} - {ts.isVisible("status") && 상태} + {ts.isVisible("pkg_code") && 품목코드} + {ts.isVisible("pkg_name") && 포장명} + {ts.isVisible("pkg_type") && 유형} + {ts.isVisible("size") && 크기(mm)} + {ts.isVisible("max_weight") && 최대중량} + {ts.isVisible("status") && 상태} @@ -496,12 +496,12 @@ export default function PackagingPage() { )} onClick={() => selectPkg(p)} > - {ts.isVisible("pkg_code") && {p.pkg_code}} - {ts.isVisible("pkg_name") && {p.pkg_name}} - {ts.isVisible("pkg_type") && {PKG_TYPE_LABEL[p.pkg_type] || p.pkg_type || "-"}} - {ts.isVisible("size") && {fmtSize(p.width_mm, p.length_mm, p.height_mm)}} - {ts.isVisible("max_weight") && {Number(p.max_load_kg || 0) > 0 ? `${p.max_load_kg}kg` : "-"}} - {ts.isVisible("status") && + {ts.isVisible("pkg_code") && {p.pkg_code}} + {ts.isVisible("pkg_name") && {p.pkg_name}} + {ts.isVisible("pkg_type") && {PKG_TYPE_LABEL[p.pkg_type] || p.pkg_type || "-"}} + {ts.isVisible("size") && {fmtSize(p.width_mm, p.length_mm, p.height_mm)}} + {ts.isVisible("max_weight") && {Number(p.max_load_kg || 0) > 0 ? `${p.max_load_kg}kg` : "-"}} + {ts.isVisible("status") && {STATUS_LABEL[p.status] || p.status} diff --git a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx index 620b1feb..0287cb14 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/receiving/page.tsx @@ -575,7 +575,7 @@ export default function ReceivingPage() {
-
+
@@ -584,21 +584,21 @@ export default function ReceivingPage() { onCheckedChange={toggleCheckAll} /> - {ts.isVisible("inbound_number") && 입고번호} - {ts.isVisible("inbound_type") && 입고유형} - {ts.isVisible("inbound_date") && 입고일} - {ts.isVisible("reference_number") && 참조번호} - {ts.isVisible("source_type") && 데이터출처} - {ts.isVisible("supplier_name") && 공급처} - {ts.isVisible("item_number") && 품목코드} - {ts.isVisible("item_name") && 품목명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("inbound_qty") && 입고수량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("total_amount") && 금액} - {ts.isVisible("warehouse_name") && 창고} - {ts.isVisible("inbound_status") && 입고상태} - {ts.isVisible("remark") && 비고} + {ts.isVisible("inbound_number") && 입고번호} + {ts.isVisible("inbound_type") && 입고유형} + {ts.isVisible("inbound_date") && 입고일} + {ts.isVisible("reference_number") && 참조번호} + {ts.isVisible("source_type") && 데이터출처} + {ts.isVisible("supplier_name") && 공급처} + {ts.isVisible("item_number") && 품목코드} + {ts.isVisible("item_name") && 품목명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("inbound_qty") && 입고수량} + {ts.isVisible("unit_price") && 단가} + {ts.isVisible("total_amount") && 금액} + {ts.isVisible("warehouse_name") && 창고} + {ts.isVisible("inbound_status") && 입고상태} + {ts.isVisible("remark") && 비고} @@ -642,10 +642,10 @@ export default function ReceivingPage() { onCheckedChange={() => toggleCheck(row.id)} /> - {ts.isVisible("inbound_number") && + {ts.isVisible("inbound_number") && {row.inbound_number} } - {ts.isVisible("inbound_type") && + {ts.isVisible("inbound_type") && } - {ts.isVisible("inbound_date") && + {ts.isVisible("inbound_date") && {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : "-"} } - {ts.isVisible("reference_number") && + {ts.isVisible("reference_number") && {row.reference_number || "-"} } - {ts.isVisible("source_type") && + {ts.isVisible("source_type") && {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} } - {ts.isVisible("supplier_name") && + {ts.isVisible("supplier_name") && {row.supplier_name || "-"} } - {ts.isVisible("item_number") && + {ts.isVisible("item_number") && {row.item_number || "-"} } - {ts.isVisible("item_name") && {row.item_name || "-"}} - {ts.isVisible("spec") && {row.spec || "-"}} - {ts.isVisible("inbound_qty") && + {ts.isVisible("item_name") && {row.item_name || "-"}} + {ts.isVisible("spec") && {row.spec || "-"}} + {ts.isVisible("inbound_qty") && {Number(row.inbound_qty || 0).toLocaleString()} } - {ts.isVisible("unit_price") && + {ts.isVisible("unit_price") && {Number(row.unit_price || 0).toLocaleString()} } - {ts.isVisible("total_amount") && + {ts.isVisible("total_amount") && {Number(row.total_amount || 0).toLocaleString()} } - {ts.isVisible("warehouse_name") && + {ts.isVisible("warehouse_name") && {row.warehouse_name || row.warehouse_code || "-"} } - {ts.isVisible("inbound_status") && + {ts.isVisible("inbound_status") && } - {ts.isVisible("remark") && + {ts.isVisible("remark") && {row.memo || "-"} } diff --git a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx index 8e59c67b..1a17a9a7 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/warehouse/page.tsx @@ -555,12 +555,12 @@ export default function WarehouseManagementPage() { 등록된 창고가 없어요 ) : ( -
+
# {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -581,7 +581,7 @@ export default function WarehouseManagementPage() { {ts.visibleColumns.map((col) => { if (col.key === "warehouse_type") { return ( - + {w.warehouse_type} @@ -590,7 +590,7 @@ export default function WarehouseManagementPage() { } if (col.key === "status") { return ( - + {w.status} @@ -598,7 +598,7 @@ export default function WarehouseManagementPage() { ); } return ( - + {w[col.key] ?? ""} ); diff --git a/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx b/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx index fd8138e2..a4cbb7ed 100644 --- a/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx +++ b/frontend/app/(main)/COMPANY_16/master-data/department/page.tsx @@ -367,14 +367,14 @@ export default function DepartmentPage() { {/* 부서 테이블 */}
-
+
No 부서코드 부서명 - {isColVisible("parent_dept_code") && 상위부서} - {isColVisible("status") && 상태} + {isColVisible("parent_dept_code") && 상위부서} + {isColVisible("status") && 상태} @@ -403,9 +403,9 @@ export default function DepartmentPage() { {idx + 1} {dept.dept_code} {dept.dept_name} - {isColVisible("parent_dept_code") && {dept.parent_dept_code || "—"}} + {isColVisible("parent_dept_code") && {dept.parent_dept_code || "—"}} {isColVisible("status") && ( - + {dept.status && ( ) : ( -
+
# {ts.visibleColumns.map((col) => ( ( 등록된 외주품목이 없어요

) : ( -
+
- {ts.isVisible("item_number") && 품번} - {ts.isVisible("item_name") && 품명} - {ts.isVisible("size") && 규격} - {ts.isVisible("unit") && 단위} - {ts.isVisible("standard_price") && 기준단가} - {ts.isVisible("selling_price") && 판매가격} - {ts.isVisible("currency_code") && 통화} - {ts.isVisible("status") && 상태} + {ts.isVisible("item_number") && 품번} + {ts.isVisible("item_name") && 품명} + {ts.isVisible("size") && 규격} + {ts.isVisible("unit") && 단위} + {ts.isVisible("standard_price") && 기준단가} + {ts.isVisible("selling_price") && 판매가격} + {ts.isVisible("currency_code") && 통화} + {ts.isVisible("status") && 상태} @@ -369,14 +369,14 @@ export default function SubcontractorItemPage() { onClick={() => setSelectedItemId(item.id)} onDoubleClick={openEditItem} > - {ts.isVisible("item_number") && {item.item_number}} - {ts.isVisible("item_name") && {item.item_name || "-"}} - {ts.isVisible("size") && {item.size || "-"}} - {ts.isVisible("unit") && {item.unit || "-"}} - {ts.isVisible("standard_price") && {formatNum(item.standard_price)}} - {ts.isVisible("selling_price") && {formatNum(item.selling_price)}} - {ts.isVisible("currency_code") && {item.currency_code || "-"}} - {ts.isVisible("status") && {item.status || "-"}} + {ts.isVisible("item_number") && {item.item_number}} + {ts.isVisible("item_name") && {item.item_name || "-"}} + {ts.isVisible("size") && {item.size || "-"}} + {ts.isVisible("unit") && {item.unit || "-"}} + {ts.isVisible("standard_price") && {formatNum(item.standard_price)}} + {ts.isVisible("selling_price") && {formatNum(item.selling_price)}} + {ts.isVisible("currency_code") && {item.currency_code || "-"}} + {ts.isVisible("status") && {item.status || "-"}} ))} diff --git a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx index 5b806f86..810f7770 100644 --- a/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx +++ b/frontend/app/(main)/COMPANY_16/outsourcing/subcontractor/page.tsx @@ -764,15 +764,15 @@ export default function SubcontractorManagementPage() {

등록된 외주업체가 없어요

) : ( -
+
- {ts.isVisible("subcontractor_code") && 외주업체코드} - {ts.isVisible("subcontractor_name") && 외주업체명} - {ts.isVisible("contact_person") && 담당자} - {ts.isVisible("contact_phone") && 연락처} - {ts.isVisible("division_label") && 유형} - {ts.isVisible("status_label") && 상태} + {ts.isVisible("subcontractor_code") && 외주업체코드} + {ts.isVisible("subcontractor_name") && 외주업체명} + {ts.isVisible("contact_person") && 담당자} + {ts.isVisible("contact_phone") && 연락처} + {ts.isVisible("division_label") && 유형} + {ts.isVisible("status_label") && 상태} @@ -788,19 +788,19 @@ export default function SubcontractorManagementPage() { onClick={() => setSelectedSubcontractorId(sub.id)} onDoubleClick={openSubcontractorEdit} > - {ts.isVisible("subcontractor_code") && {sub.subcontractor_code}} - {ts.isVisible("subcontractor_name") && {sub.subcontractor_name}} - {ts.isVisible("contact_person") && {sub.contact_person || "-"}} - {ts.isVisible("contact_phone") && {sub.contact_phone || "-"}} + {ts.isVisible("subcontractor_code") && {sub.subcontractor_code}} + {ts.isVisible("subcontractor_name") && {sub.subcontractor_name}} + {ts.isVisible("contact_person") && {sub.contact_person || "-"}} + {ts.isVisible("contact_phone") && {sub.contact_phone || "-"}} {ts.isVisible("division_label") && ( - + {sub.division_label ? {sub.division_label} : "-"} )} {ts.isVisible("status_label") && ( - + {sub.status_label ? {sub.status_label} : "-"} diff --git a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx index 5a27642d..d3fb7be1 100644 --- a/frontend/app/(main)/COMPANY_16/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/bom/page.tsx @@ -955,7 +955,7 @@ export default function BomManagementPage() {

등록된 BOM이 없어요

) : ( -
+
@@ -967,7 +967,7 @@ export default function BomManagementPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -995,15 +995,15 @@ export default function BomManagementPage() { {ts.visibleColumns.map((col) => { if (col.key === "item_code") { - return {row.item_code || row.item_number || "-"}; + return {row.item_code || row.item_number || "-"}; } if (col.key === "bom_type") { - return {BOM_TYPE_OPTIONS.find((o) => o.code === row.bom_type)?.label || row.bom_type || "-"}; + return {BOM_TYPE_OPTIONS.find((o) => o.code === row.bom_type)?.label || row.bom_type || "-"}; } if (col.key === "status") { - return {renderStatusBadge(row.status)}; + return {renderStatusBadge(row.status)}; } - return {row[col.key] || "-"}; + return {row[col.key] || "-"}; })} ))} diff --git a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx index 49640fe7..aeff278d 100644 --- a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx @@ -1019,7 +1019,7 @@ export default function ProductionPlanManagementPage() { ) : (
-
+
@@ -1028,15 +1028,15 @@ export default function ProductionPlanManagementPage() { 품목코드 품목명 - {isColVisible("total_order_qty") && 총수주량} - {isColVisible("total_ship_qty") && 출고량} - {isColVisible("total_balance_qty") && 잔량} - {isColVisible("current_stock") && 현재고} - {isColVisible("safety_stock") && 안전재고} - {isColVisible("existing_plan_qty") && 기생산계획량} - {isColVisible("in_progress_qty") && 생산진행} - {isColVisible("required_plan_qty") && 필요생산계획} - {isColVisible("lead_time") && 리드타임(일)} + {isColVisible("total_order_qty") && 총수주량} + {isColVisible("total_ship_qty") && 출고량} + {isColVisible("total_balance_qty") && 잔량} + {isColVisible("current_stock") && 현재고} + {isColVisible("safety_stock") && 안전재고} + {isColVisible("existing_plan_qty") && 기생산계획량} + {isColVisible("in_progress_qty") && 생산진행} + {isColVisible("required_plan_qty") && 필요생산계획} + {isColVisible("lead_time") && 리드타임(일)} @@ -1051,20 +1051,20 @@ export default function ProductionPlanManagementPage() { toggleItemExpand(item.item_code)}>{item.item_code} toggleItemExpand(item.item_code)}>{item.item_name} - {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} - {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} - {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} - {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} - {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} - {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} - {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} + {isColVisible("total_order_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_order_qty)}} + {isColVisible("total_ship_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_ship_qty)}} + {isColVisible("total_balance_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.total_balance_qty)}} + {isColVisible("current_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.current_stock)}} + {isColVisible("safety_stock") && toggleItemExpand(item.item_code)}>{formatNumber(item.safety_stock)}} + {isColVisible("existing_plan_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.existing_plan_qty)}} + {isColVisible("in_progress_qty") && toggleItemExpand(item.item_code)}>{formatNumber(item.in_progress_qty)}} {isColVisible("required_plan_qty") && ( - 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> + 0 ? "text-destructive" : "text-success")} onClick={() => toggleItemExpand(item.item_code)}> {formatNumber(item.required_plan_qty)} )} {isColVisible("lead_time") && ( - toggleItemExpand(item.item_code)}> + toggleItemExpand(item.item_code)}> {Number(item.lead_time) > 0 ? `${item.lead_time}일` : "-"} )} @@ -1084,9 +1084,9 @@ export default function ProductionPlanManagementPage() { - {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} - {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} - {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} + {isColVisible("total_order_qty") && {formatNumber(detail.order_qty)}} + {isColVisible("total_ship_qty") && {formatNumber(detail.ship_qty)}} + {isColVisible("total_balance_qty") && {formatNumber(detail.balance_qty)}} 납기일: {detail.due_date || "-"} diff --git a/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx b/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx index 7bbf3870..b70ab356 100644 --- a/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/work-instruction/page.tsx @@ -446,22 +446,22 @@ export default function WorkInstructionPage() { {/* 테이블 */}
-
+
- {ts.isVisible("work_instruction_no") && 작업지시번호} - {ts.isVisible("status") && 상태} - {ts.isVisible("progress") && 진행현황} - {ts.isVisible("item_name") && 품목명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("qty") && 수량} - {ts.isVisible("equipment") && 설비} - {ts.isVisible("routing") && 라우팅} - {ts.isVisible("work_team") && 작업조} - {ts.isVisible("worker") && 작업자} - {ts.isVisible("start_date") && 시작일} - {ts.isVisible("end_date") && 완료일} - {ts.isVisible("actions") && 작업} + {ts.isVisible("work_instruction_no") && 작업지시번호} + {ts.isVisible("status") && 상태} + {ts.isVisible("progress") && 진행현황} + {ts.isVisible("item_name") && 품목명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("qty") && 수량} + {ts.isVisible("equipment") && 설비} + {ts.isVisible("routing") && 라우팅} + {ts.isVisible("work_team") && 작업조} + {ts.isVisible("worker") && 작업자} + {ts.isVisible("start_date") && 시작일} + {ts.isVisible("end_date") && 완료일} + {ts.isVisible("actions") && 작업} @@ -487,9 +487,9 @@ export default function WorkInstructionPage() { const isFirstOfGroup = Number(o.detail_seq) === 1; return ( - {ts.isVisible("work_instruction_no") && {getDisplayNo(o)}} - {ts.isVisible("status") && {sBadge.label}} - {ts.isVisible("progress") && + {ts.isVisible("work_instruction_no") && {getDisplayNo(o)}} + {ts.isVisible("status") && {sBadge.label}} + {ts.isVisible("progress") && {isFirstOfGroup ? (
{pBadge.label} @@ -500,11 +500,11 @@ export default function WorkInstructionPage() {
) : }
} - {ts.isVisible("item_name") && {o.item_name || o.item_number || "-"}} - {ts.isVisible("spec") && {o.item_spec || "-"}} - {ts.isVisible("qty") && {Number(o.detail_qty || 0).toLocaleString()}} - {ts.isVisible("equipment") && {isFirstOfGroup ? (o.equipment_name || "-") : ""}} - {ts.isVisible("routing") && + {ts.isVisible("item_name") && {o.item_name || o.item_number || "-"}} + {ts.isVisible("spec") && {o.item_spec || "-"}} + {ts.isVisible("qty") && {Number(o.detail_qty || 0).toLocaleString()}} + {ts.isVisible("equipment") && {isFirstOfGroup ? (o.equipment_name || "-") : ""}} + {ts.isVisible("routing") && {isFirstOfGroup ? ( o.routing_version_id ? ( diff --git a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx index f746596f..0f0fbfa1 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx @@ -639,7 +639,7 @@ export default function PurchaseOrderPage() { {/* 데이터 테이블 */}
-
+
@@ -651,20 +651,20 @@ export default function PurchaseOrderPage() { }} /> - {ts.isVisible("purchase_no") && 발주번호} - {ts.isVisible("order_date") && 발주일} - {ts.isVisible("supplier_name") && 공급업체} - {ts.isVisible("item_code") && 품번} - {ts.isVisible("item_name") && 품명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("order_qty") && 발주수량} - {ts.isVisible("received_qty") && 입고수량} - {ts.isVisible("remain_qty") && 잔량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("amount") && 금액} - {ts.isVisible("due_date") && 납기일} - {ts.isVisible("status") && 상태} - {ts.isVisible("memo") && 메모} + {ts.isVisible("purchase_no") && 발주번호} + {ts.isVisible("order_date") && 발주일} + {ts.isVisible("supplier_name") && 공급업체} + {ts.isVisible("item_code") && 품번} + {ts.isVisible("item_name") && 품명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("order_qty") && 발주수량} + {ts.isVisible("received_qty") && 입고수량} + {ts.isVisible("remain_qty") && 잔량} + {ts.isVisible("unit_price") && 단가} + {ts.isVisible("amount") && 금액} + {ts.isVisible("due_date") && 납기일} + {ts.isVisible("status") && 상태} + {ts.isVisible("memo") && 메모} @@ -696,20 +696,20 @@ export default function PurchaseOrderPage() { }} /> - {ts.isVisible("purchase_no") && {row.purchase_no}} - {ts.isVisible("order_date") && {row.order_date}} - {ts.isVisible("supplier_name") && {row.supplier_name}} - {ts.isVisible("item_code") && {row.item_code}} - {ts.isVisible("item_name") && {row.item_name}} - {ts.isVisible("spec") && {row.spec}} - {ts.isVisible("order_qty") && {row.order_qty ? Number(row.order_qty).toLocaleString() : ""}} - {ts.isVisible("received_qty") && {row.received_qty ? Number(row.received_qty).toLocaleString() : ""}} - {ts.isVisible("remain_qty") && {row.remain_qty ? Number(row.remain_qty).toLocaleString() : ""}} - {ts.isVisible("unit_price") && {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}} - {ts.isVisible("amount") && {row.amount ? Number(row.amount).toLocaleString() : ""}} - {ts.isVisible("due_date") && {row.due_date}} + {ts.isVisible("purchase_no") && {row.purchase_no}} + {ts.isVisible("order_date") && {row.order_date}} + {ts.isVisible("supplier_name") && {row.supplier_name}} + {ts.isVisible("item_code") && {row.item_code}} + {ts.isVisible("item_name") && {row.item_name}} + {ts.isVisible("spec") && {row.spec}} + {ts.isVisible("order_qty") && {row.order_qty ? Number(row.order_qty).toLocaleString() : ""}} + {ts.isVisible("received_qty") && {row.received_qty ? Number(row.received_qty).toLocaleString() : ""}} + {ts.isVisible("remain_qty") && {row.remain_qty ? Number(row.remain_qty).toLocaleString() : ""}} + {ts.isVisible("unit_price") && {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}} + {ts.isVisible("amount") && {row.amount ? Number(row.amount).toLocaleString() : ""}} + {ts.isVisible("due_date") && {row.due_date}} {ts.isVisible("status") && ( - + {row.status && ( {row.status} @@ -717,7 +717,7 @@ export default function PurchaseOrderPage() { )} )} - {ts.isVisible("memo") && {row.memo}} + {ts.isVisible("memo") && {row.memo}} ))} diff --git a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx index a46de4d0..b730842a 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/purchase-item/page.tsx @@ -381,15 +381,15 @@ export default function PurchaseItemPage() {
-
+
품번 품명 - {isColVisible("size") && 규격} - {isColVisible("unit") && 단위} - {isColVisible("standard_price") && 기준단가} - {isColVisible("status") && 상태} + {isColVisible("size") && 규격} + {isColVisible("unit") && 단위} + {isColVisible("standard_price") && 기준단가} + {isColVisible("status") && 상태} @@ -409,11 +409,11 @@ export default function PurchaseItemPage() { > {item.item_number} {item.item_name} - {isColVisible("size") && {item.size || "-"}} - {isColVisible("unit") && {item.unit || "-"}} - {isColVisible("standard_price") && {item.standard_price ? Number(item.standard_price).toLocaleString() : "-"}} + {isColVisible("size") && {item.size || "-"}} + {isColVisible("unit") && {item.unit || "-"}} + {isColVisible("standard_price") && {item.standard_price ? Number(item.standard_price).toLocaleString() : "-"}} {isColVisible("status") && ( - + {item.status || "-"} diff --git a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx index 1a45c38c..8c06cbb8 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/supplier/page.tsx @@ -370,14 +370,14 @@ export default function SupplierManagementPage() {
-
+
공급업체코드 공급업체명 - {isColVisible("contact_person") && 담당자} - {isColVisible("contact_phone") && 연락처} - {isColVisible("status") && 상태} + {isColVisible("contact_person") && 담당자} + {isColVisible("contact_phone") && 연락처} + {isColVisible("status") && 상태} @@ -397,10 +397,10 @@ export default function SupplierManagementPage() { > {s.supplier_code} {s.supplier_name} - {isColVisible("contact_person") && {s.contact_person || "-"}} - {isColVisible("contact_phone") && {s.contact_phone || "-"}} + {isColVisible("contact_person") && {s.contact_person || "-"}} + {isColVisible("contact_phone") && {s.contact_phone || "-"}} {isColVisible("status") && ( - + {s.status || "-"} diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx index be480fcc..107905e9 100644 --- a/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/inspection/page.tsx @@ -353,7 +353,7 @@ export default function InspectionManagementPage() { />
-
+
@@ -363,7 +363,7 @@ export default function InspectionManagementPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -383,10 +383,10 @@ export default function InspectionManagementPage() { setInspChecked(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> {ts.visibleColumns.map((col) => { - if (col.key === "inspection_type") return {getCatLabel(INSPECTION_TABLE, "inspection_type", row.inspection_type)}; - if (col.key === "apply_type") return {getCatLabel(INSPECTION_TABLE, "apply_type", row.apply_type)}; - if (col.key === "is_active") return {row.is_active ? "사용" : "미사용"}; - return {row[col.key] ?? ""}; + if (col.key === "inspection_type") return {getCatLabel(INSPECTION_TABLE, "inspection_type", row.inspection_type)}; + if (col.key === "apply_type") return {getCatLabel(INSPECTION_TABLE, "apply_type", row.apply_type)}; + if (col.key === "is_active") return {row.is_active ? "사용" : "미사용"}; + return {row[col.key] ?? ""}; })} ))} diff --git a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx index 1a62164d..93cd788e 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -165,7 +165,7 @@ export default function ItemInspectionInfoPage() {
-
+
@@ -175,7 +175,7 @@ export default function ItemInspectionInfoPage() { /> {ts.visibleColumns.map((col) => ( - {col.label} + {col.label} ))} @@ -195,7 +195,7 @@ export default function ItemInspectionInfoPage() { setCheckedIds(prev => v ? [...prev, row.id] : prev.filter(id => id !== row.id))} /> {ts.visibleColumns.map((col) => ( - + {col.key === "is_active" ? {row.is_active ? "사용" : "미사용"} : row[col.key] ?? ""} diff --git a/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx b/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx index a7f3763e..78b0d1ca 100644 --- a/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/claim/page.tsx @@ -464,12 +464,12 @@ export default function ClaimManagementPage() { {/* 테이블 */}
-
+
# {ts.visibleColumns.map((col) => ( - + {col.label} ))} @@ -513,7 +513,7 @@ export default function ClaimManagementPage() { {ts.visibleColumns.map((col) => { if (col.key === "claim_type") { return ( - + {claim.claim_type} @@ -522,7 +522,7 @@ export default function ClaimManagementPage() { } if (col.key === "claim_status") { return ( - + {claim.claim_status} @@ -531,13 +531,13 @@ export default function ClaimManagementPage() { } if (col.key === "claim_content") { return ( - + {claim.claim_content} ); } return ( - + {claim[col.key] ?? "-"} ); diff --git a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx index f5a4061f..f888bddf 100644 --- a/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/customer/page.tsx @@ -915,16 +915,16 @@ export default function CustomerManagementPage() { {/* 거래처 테이블 */}
-
+
No - {isColumnVisible("customer_code") && 거래처코드} - {isColumnVisible("customer_name") && 거래처명} - {isColumnVisible("contact_person") && 대표자} - {isColumnVisible("contact_phone") && 연락처} - {isColumnVisible("division") && 유형} - {isColumnVisible("status") && 상태} + {isColumnVisible("customer_code") && 거래처코드} + {isColumnVisible("customer_name") && 거래처명} + {isColumnVisible("contact_person") && 대표자} + {isColumnVisible("contact_phone") && 연락처} + {isColumnVisible("division") && 유형} + {isColumnVisible("status") && 상태} @@ -951,12 +951,12 @@ export default function CustomerManagementPage() { onDoubleClick={() => { setSelectedCustomerId(c.id); openCustomerEdit(); }} > {idx + 1} - {isColumnVisible("customer_code") && {c.customer_code}} - {isColumnVisible("customer_name") && {c.customer_name}} - {isColumnVisible("contact_person") && {c.contact_person}} - {isColumnVisible("contact_phone") && {c.contact_phone}} + {isColumnVisible("customer_code") && {c.customer_code}} + {isColumnVisible("customer_name") && {c.customer_name}} + {isColumnVisible("contact_person") && {c.contact_person}} + {isColumnVisible("contact_phone") && {c.contact_phone}} {isColumnVisible("division") && ( - + {c.division && ( {c.division} @@ -965,7 +965,7 @@ export default function CustomerManagementPage() { )} {isColumnVisible("status") && ( - + {c.status && (
-
+
@@ -646,19 +646,19 @@ export default function SalesOrderPage() { className="h-4 w-4 cursor-pointer rounded border-border accent-primary" /> - {ts.isVisible("order_no") && 수주번호} - {ts.isVisible("part_code") && 품번} - {ts.isVisible("part_name") && 품명} - {ts.isVisible("spec") && 규격} - {ts.isVisible("unit") && 단위} - {ts.isVisible("qty") && 수량} - {ts.isVisible("ship_qty") && 출하수량} - {ts.isVisible("balance_qty") && 잔량} - {ts.isVisible("unit_price") && 단가} - {ts.isVisible("amount") && 금액} - {ts.isVisible("currency_code") && 통화} - {ts.isVisible("due_date") && 납기일} - {ts.isVisible("memo") && 메모} + {ts.isVisible("order_no") && 수주번호} + {ts.isVisible("part_code") && 품번} + {ts.isVisible("part_name") && 품명} + {ts.isVisible("spec") && 규격} + {ts.isVisible("unit") && 단위} + {ts.isVisible("qty") && 수량} + {ts.isVisible("ship_qty") && 출하수량} + {ts.isVisible("balance_qty") && 잔량} + {ts.isVisible("unit_price") && 단가} + {ts.isVisible("amount") && 금액} + {ts.isVisible("currency_code") && 통화} + {ts.isVisible("due_date") && 납기일} + {ts.isVisible("memo") && 메모} @@ -695,28 +695,28 @@ export default function SalesOrderPage() { - {ts.isVisible("order_no") && {row.order_no}} + {ts.isVisible("order_no") && {row.order_no}} {ts.isVisible("part_code") && ( - + {row.part_code} )} {ts.isVisible("part_name") && ( - + {row.part_name} )} - {ts.isVisible("spec") && {row.spec}} - {ts.isVisible("unit") && {row.unit}} - {ts.isVisible("qty") && {row.qty ? Number(row.qty).toLocaleString() : ""}} - {ts.isVisible("ship_qty") && {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""}} - {ts.isVisible("balance_qty") && {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""}} - {ts.isVisible("unit_price") && {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}} - {ts.isVisible("amount") && {row.amount ? Number(row.amount).toLocaleString() : ""}} - {ts.isVisible("currency_code") && {row.currency_code}} - {ts.isVisible("due_date") && {row.due_date}} + {ts.isVisible("spec") && {row.spec}} + {ts.isVisible("unit") && {row.unit}} + {ts.isVisible("qty") && {row.qty ? Number(row.qty).toLocaleString() : ""}} + {ts.isVisible("ship_qty") && {row.ship_qty ? Number(row.ship_qty).toLocaleString() : ""}} + {ts.isVisible("balance_qty") && {row.balance_qty ? Number(row.balance_qty).toLocaleString() : ""}} + {ts.isVisible("unit_price") && {row.unit_price ? Number(row.unit_price).toLocaleString() : ""}} + {ts.isVisible("amount") && {row.amount ? Number(row.amount).toLocaleString() : ""}} + {ts.isVisible("currency_code") && {row.currency_code}} + {ts.isVisible("due_date") && {row.due_date}} {ts.isVisible("memo") && ( - + {row.memo} )} diff --git a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx index 05c536f8..86ab3f0d 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx @@ -460,7 +460,7 @@ export default function ShippingOrderPage() { ) : ( -
+
@@ -469,18 +469,18 @@ export default function ShippingOrderPage() { onCheckedChange={handleCheckAll} /> - {ts.isVisible("instruction_no") && 출하지시번호} - {ts.isVisible("ship_date") && 출하일자} - {ts.isVisible("customer_name") && 거래처명} - {ts.isVisible("transport_company") && 운송업체} - {ts.isVisible("vehicle_no") && 차량번호} - {ts.isVisible("driver_name") && 기사명} - {ts.isVisible("status") && 상태} - {ts.isVisible("item_code") && 품번} - {ts.isVisible("item_name") && 품명} - {ts.isVisible("qty") && 수량} - {ts.isVisible("source_type") && 소스} - {ts.isVisible("remark") && 비고} + {ts.isVisible("instruction_no") && 출하지시번호} + {ts.isVisible("ship_date") && 출하일자} + {ts.isVisible("customer_name") && 거래처명} + {ts.isVisible("transport_company") && 운송업체} + {ts.isVisible("vehicle_no") && 차량번호} + {ts.isVisible("driver_name") && 기사명} + {ts.isVisible("status") && 상태} + {ts.isVisible("item_code") && 품번} + {ts.isVisible("item_name") && 품명} + {ts.isVisible("qty") && 수량} + {ts.isVisible("source_type") && 소스} + {ts.isVisible("remark") && 비고} @@ -516,22 +516,22 @@ export default function ShippingOrderPage() { }} /> - {ts.isVisible("instruction_no") && {order.instruction_no}} - {ts.isVisible("ship_date") && {formatDate(order.instruction_date)}} - {ts.isVisible("customer_name") && {order.customer_name || "-"}} - {ts.isVisible("transport_company") && {order.carrier_name || "-"}} - {ts.isVisible("vehicle_no") && {order.vehicle_no || "-"}} - {ts.isVisible("driver_name") && {order.driver_name || "-"}} - {ts.isVisible("status") && + {ts.isVisible("instruction_no") && {order.instruction_no}} + {ts.isVisible("ship_date") && {formatDate(order.instruction_date)}} + {ts.isVisible("customer_name") && {order.customer_name || "-"}} + {ts.isVisible("transport_company") && {order.carrier_name || "-"}} + {ts.isVisible("vehicle_no") && {order.vehicle_no || "-"}} + {ts.isVisible("driver_name") && {order.driver_name || "-"}} + {ts.isVisible("status") && {getStatusLabel(order.status)} } - {ts.isVisible("item_code") && -} - {ts.isVisible("item_name") && -} - {ts.isVisible("qty") && 0} - {ts.isVisible("source_type") && -} - {ts.isVisible("remark") && {order.memo || "-"}} + {ts.isVisible("item_code") && -} + {ts.isVisible("item_name") && -} + {ts.isVisible("qty") && 0} + {ts.isVisible("source_type") && -} + {ts.isVisible("remark") && {order.memo || "-"}} ); } @@ -553,29 +553,29 @@ export default function ShippingOrderPage() { /> )} - {ts.isVisible("instruction_no") && {itemIdx === 0 ? order.instruction_no : ""}} - {ts.isVisible("ship_date") && {itemIdx === 0 ? formatDate(order.instruction_date) : ""}} - {ts.isVisible("customer_name") && {itemIdx === 0 ? (order.customer_name || "-") : ""}} - {ts.isVisible("transport_company") && {itemIdx === 0 ? (order.carrier_name || "-") : ""}} - {ts.isVisible("vehicle_no") && {itemIdx === 0 ? (order.vehicle_no || "-") : ""}} - {ts.isVisible("driver_name") && {itemIdx === 0 ? (order.driver_name || "-") : ""}} - {ts.isVisible("status") && + {ts.isVisible("instruction_no") && {itemIdx === 0 ? order.instruction_no : ""}} + {ts.isVisible("ship_date") && {itemIdx === 0 ? formatDate(order.instruction_date) : ""}} + {ts.isVisible("customer_name") && {itemIdx === 0 ? (order.customer_name || "-") : ""}} + {ts.isVisible("transport_company") && {itemIdx === 0 ? (order.carrier_name || "-") : ""}} + {ts.isVisible("vehicle_no") && {itemIdx === 0 ? (order.vehicle_no || "-") : ""}} + {ts.isVisible("driver_name") && {itemIdx === 0 ? (order.driver_name || "-") : ""}} + {ts.isVisible("status") && {itemIdx === 0 && ( {getStatusLabel(order.status)} )} } - {ts.isVisible("item_code") && {item.item_code}} - {ts.isVisible("item_name") && {item.item_name}} - {ts.isVisible("qty") && {Number(item.order_qty || 0).toLocaleString()}} - {ts.isVisible("source_type") && + {ts.isVisible("item_code") && {item.item_code}} + {ts.isVisible("item_name") && {item.item_name}} + {ts.isVisible("qty") && {Number(item.order_qty || 0).toLocaleString()}} + {ts.isVisible("source_type") && {(() => { const b = getSourceBadge(item.source_type || ""); return {b.label}; })()} } - {ts.isVisible("remark") && + {ts.isVisible("remark") && {itemIdx === 0 ? (order.memo || "-") : ""} } diff --git a/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx index f9089bc9..1a47e986 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx @@ -233,7 +233,7 @@ export default function ShippingPlanPage() { {/* 테이블 */}
-
+
@@ -242,15 +242,15 @@ export default function ShippingPlanPage() { onCheckedChange={handleCheckAll} /> - {ts.isVisible("order_no") && 수주번호} - {ts.isVisible("due_date") && 납기일} - {ts.isVisible("customer_name") && 거래처} - {ts.isVisible("part_code") && 품목코드} - {ts.isVisible("part_name") && 품목명} - {ts.isVisible("order_qty") && 수주수량} - {ts.isVisible("plan_qty") && 계획수량} - {ts.isVisible("plan_date") && 계획일} - {ts.isVisible("status") && 상태} + {ts.isVisible("order_no") && 수주번호} + {ts.isVisible("due_date") && 납기일} + {ts.isVisible("customer_name") && 거래처} + {ts.isVisible("part_code") && 품목코드} + {ts.isVisible("part_name") && 품목명} + {ts.isVisible("order_qty") && 수주수량} + {ts.isVisible("plan_qty") && 계획수량} + {ts.isVisible("plan_date") && 계획일} + {ts.isVisible("status") && 상태} @@ -293,21 +293,21 @@ export default function ShippingPlanPage() { /> )} - {ts.isVisible("order_no") && + {ts.isVisible("order_no") && {planIdx === 0 ? (plan.order_no || "-") : ""} } - {ts.isVisible("due_date") && + {ts.isVisible("due_date") && {planIdx === 0 ? formatDate(plan.due_date) : ""} } - {ts.isVisible("customer_name") && + {ts.isVisible("customer_name") && {planIdx === 0 ? (plan.customer_name || "-") : ""} } - {ts.isVisible("part_code") && {plan.part_code || "-"}} - {ts.isVisible("part_name") && {plan.part_name || "-"}} - {ts.isVisible("order_qty") && {formatNumber(plan.order_qty)}} - {ts.isVisible("plan_qty") && {formatNumber(plan.plan_qty)}} - {ts.isVisible("plan_date") && {formatDate(plan.plan_date)}} - {ts.isVisible("status") && + {ts.isVisible("part_code") && {plan.part_code || "-"}} + {ts.isVisible("part_name") && {plan.part_name || "-"}} + {ts.isVisible("order_qty") && {formatNumber(plan.order_qty)}} + {ts.isVisible("plan_qty") && {formatNumber(plan.plan_qty)}} + {ts.isVisible("plan_date") && {formatDate(plan.plan_date)}} + {ts.isVisible("status") && {getStatusLabel(plan.status)} diff --git a/frontend/components/common/TableSettingsModal.tsx b/frontend/components/common/TableSettingsModal.tsx index 87442868..e46306aa 100644 --- a/frontend/components/common/TableSettingsModal.tsx +++ b/frontend/components/common/TableSettingsModal.tsx @@ -57,12 +57,20 @@ export interface GroupSetting { enabled: boolean; } +export interface BaseFilter { + columnName: string; + operator: "equals" | "contains" | "in"; + value: string; +} + export interface TableSettings { columns: ColumnSetting[]; filters: FilterSetting[]; groups: GroupSetting[]; frozenCount: number; groupSumEnabled: boolean; + /** 기본 데이터 필터 (예: division = '판매') */ + baseFilter?: BaseFilter; } export interface TableSettingsModalProps { @@ -181,16 +189,17 @@ function SortableColumnRow({
{col.columnName}
- {/* 너비 입력 */} + {/* 너비 입력 (0 = 자동) */}
너비: onWidthChange(col._idx, Number(e.target.value) || 100)} + value={col.width || ""} + onChange={(e) => onWidthChange(col._idx, Number(e.target.value) || 0)} className="h-8 w-[70px] text-xs text-center" - min={50} + min={0} max={500} + placeholder="자동" />
@@ -217,6 +226,8 @@ export function TableSettingsModal({ const [tempGroups, setTempGroups] = useState([]); const [tempFrozenCount, setTempFrozenCount] = useState(0); const [tempGroupSum, setTempGroupSum] = useState(false); + const [tempBaseFilter, setTempBaseFilter] = useState(); + const [baseFilterOptions, setBaseFilterOptions] = useState<{ label: string; value: string }[]>([]); // 원본 컬럼 (초기화용) const [defaultColumns, setDefaultColumns] = useState([]); @@ -245,7 +256,7 @@ export function TableSettingsModal({ columnName: t.columnName, displayName: t.displayName || t.columnLabel || t.columnName, visible: defaultVisibleKeys ? defaultVisibleKeys.includes(t.columnName) : true, - width: 120, + width: 0, // 0 = 자동 너비 })); // 활성 컬럼을 GRID_COLUMNS 순서대로 위에, 비활성을 아래에 정렬 @@ -310,12 +321,14 @@ export function TableSettingsModal({ })); setTempFrozenCount(saved.frozenCount || 0); setTempGroupSum(saved.groupSumEnabled || false); + setTempBaseFilter(saved.baseFilter); } else { setTempColumns(freshColumns); setTempFilters(freshFilters); setTempGroups(freshGroups); setTempFrozenCount(0); setTempGroupSum(false); + setTempBaseFilter(undefined); } } catch (err) { console.error("테이블 설정 로드 실패:", err); @@ -332,6 +345,7 @@ export function TableSettingsModal({ groups: tempGroups, frozenCount: tempFrozenCount, groupSumEnabled: tempGroupSum, + baseFilter: tempBaseFilter, }; localStorage.setItem(getStorageKey(settingsId), JSON.stringify(settings)); onSave?.(settings); @@ -499,32 +513,41 @@ export function TableSettingsModal({ {/* ===== 탭 2: 필터 설정 ===== */} - {/* 전체 선택 */} -
toggleFilterAll(!allFiltersEnabled)} - > - - 전체 선택 + {/* 검색 필터 설정 (상단) */} +
+
toggleFilterAll(!allFiltersEnabled)} + > + + 검색 필터 +
+ + {tempFilters.filter((f) => f.enabled).length}/{tempFilters.length}개 활성 +
- {/* 필터 목록 */} -
+ {/* 필터 목록 — 2열 그리드 */} +
{tempFilters.map((filter, idx) => (
toggleFilter(idx)} > - toggleFilter(idx)} - /> +
{filter.displayName}
-
- changeFilterWidth(idx, Number(e.target.value) || 25)} - className="h-8 w-[55px] text-xs text-center" - min={10} - max={100} - /> - % -
))}
+ {/* 기본 데이터 필터 (하단) */} +
+
+
기본 데이터 필터
+
+ 이 화면에서 항상 적용되는 데이터 필터 조건 +
+
+ + {tempBaseFilter ? ( +
+
+ {tableName} + {" — "} + {tempBaseFilter.columnName} + {" = "} + {tempBaseFilter.value || "(미설정)"} +
+ +
+ ) : ( +
필터 없음
+ )} + +
+ + + +
+
+ {/* 그룹별 합산 토글 */} -
+
그룹별 합산
같은 값끼리 그룹핑하여 합산
diff --git a/frontend/components/ui/popover.tsx b/frontend/components/ui/popover.tsx index c9138ac2..29d4a2f1 100644 --- a/frontend/components/ui/popover.tsx +++ b/frontend/components/ui/popover.tsx @@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef< align={align} sideOffset={sideOffset} 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, )} {...props} diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx index 1ebfdfed..0b34a6b2 100644 --- a/frontend/components/ui/select.tsx +++ b/frontend/components/ui/select.tsx @@ -62,7 +62,7 @@ function SelectContent({ */ -import { useState, useEffect, useCallback, useMemo } from "react"; -import { loadTableSettings, type TableSettings } from "@/components/common/TableSettingsModal"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { loadTableSettings, type TableSettings, type BaseFilter } from "@/components/common/TableSettingsModal"; export function useTableSettings( settingsId: string, @@ -43,6 +43,8 @@ export function useTableSettings( const [orderedKeys, setOrderedKeys] = useState( () => defaultColumns.map((c) => c.key), ); + const [baseFilter, setBaseFilter] = useState(); + // 초기 filterConfig: GRID_COLUMNS에 있는 컬럼만 필터 가능 (전부 비활성) const [filterConfig, setFilterConfig] = useState( () => @@ -86,6 +88,9 @@ export function useTableSettings( setFilterConfig( settings.filters?.filter((f) => visible.has(f.columnName)), ); + + // 기본 데이터 필터 + setBaseFilter(settings.baseFilter); }, [defaultColumns], ); @@ -128,6 +133,16 @@ export function useTableSettings( [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 { /** 모달 open 상태 */ open, @@ -145,8 +160,12 @@ export function useTableSettings( isVisible, /** 특정 컬럼 너비 (px) */ getWidth, + /** TableHead/TableCell style 객체 반환 */ + thStyle, /** 필터 설정 */ filterConfig, + /** 기본 데이터 필터 (예: division = '판매') */ + baseFilter, /** GRID_COLUMNS 기본 컬럼 키 목록 (TableSettingsModal defaultVisibleKeys용) */ defaultVisibleKeys: defaultColumns.map((c) => c.key), };