feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가

자재관리:
- 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장
- 재고현황 컴포넌트 리팩토링

출고관리:
- 출하관리 생성/수정/목록/상세 개선
- 차량배차관리 상세/수정/목록 기능 보강

생산관리:
- 작업지시서 WIP 생산 모달 신규 추가
- 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가
- 작업자화면 기능 대폭 확장 (카드/목록 개선)
- 검사성적서 모달 개선

품질관리:
- 실적보고서 관리 페이지 신규 추가
- 검사관리 문서/타입/목데이터 개선

단가관리:
- 단가배포 페이지 및 컴포넌트 신규 추가
- 단가표 관리 페이지 및 컴포넌트 신규 추가

공통:
- 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard)
- 메뉴 폴링 훅 개선, 레이아웃 수정
- 모바일 줌/패닝 CSS 수정
- locale 유틸 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-04 12:46:19 +09:00
parent 17c16028b1
commit c1b63b850a
70 changed files with 6832 additions and 384 deletions

View File

@@ -0,0 +1,67 @@
# [FIX] 모바일 핀치 줌 후 좌우 패닝 안 되는 문제
## 문제
- 모바일(iOS Safari, Android Chrome 동일)에서 핀치 줌으로 확대 후 **상하 스크롤은 되지만 좌우 이동(패닝)이 안 됨**
## 원인 분석
### 근본 원인: 모바일 레이아웃의 app-shell 패턴
`AuthenticatedLayout.tsx`의 모바일 레이아웃이 **자체 스크롤 컨테이너**를 만들어 브라우저의 뷰포트 패닝을 가로챔.
#### 문제가 된 3가지 요소
| 요소 | 코드 | 문제 |
|------|------|------|
| 부모 div 고정 높이 | `style={{ height: 'var(--app-height)' }}` | 레이아웃을 뷰포트에 가둠 |
| main overflow | `overflow-y-auto` / `overflow-auto` | main을 스크롤 컨테이너로 만듦 |
| overscroll 차단 | `overscroll-contain` | 스크롤 이벤트 전파 차단 |
#### 동작 메커니즘
1. 핀치 줌 → 브라우저 visual viewport 확대
2. 좌우 패닝 시도 → 터치 이벤트가 `<main>` 스크롤 컨테이너에 도달
3. `<main>`에 가로 오버플로우 콘텐츠 없음 → 스크롤 불가
4. `overscroll-behavior: contain` → 브라우저 뷰포트 패닝으로 전파 차단
5. 결과: 좌우 이동 불가
### 시도했지만 효과 없었던 방법들
- `touchAction: 'pan-x pan-y pinch-zoom'` → 스크롤 컨테이너가 이벤트 가로채서 무효
- `touchAction: 'manipulation'` → 동일
- `html { touch-action: manipulation }` → 하위 스크롤 컨테이너가 우선
- `overscrollBehaviorX: auto` 단독 적용 → 부모 고정 높이가 여전히 제약
## 해결
### 변경 사항
**`src/layouts/AuthenticatedLayout.tsx`**
```tsx
// Before
<div className="flex flex-col bg-background min-h-screen" style={{ height: 'var(--app-height)' }}>
...
<main className="flex-1 overflow-y-auto px-3 overscroll-contain" style={{ WebkitOverflowScrolling: 'touch', touchAction: 'pan-y pinch-zoom' }}>
// After
<div className="flex flex-col bg-background min-h-screen">
...
<main className="flex-1 px-3">
```
**`src/app/globals.css`** (추가)
```css
html {
touch-action: manipulation;
}
```
### 핵심 원리
- 부모 div의 `height: var(--app-height)` 제거 → 레이아웃이 자연스럽게 확장
- `<main>``overflow-auto`, `overscroll-contain` 제거 → 스크롤 컨테이너 해제
- 스크롤이 body/html 레벨로 이동 → 브라우저가 줌 패닝 정상 처리
### 확인 사항
- [x] 핀치 줌 후 좌우 패닝
- [ ] 일반 상하 스크롤
- [ ] 헤더 sticky 유지
- [ ] 데스크톱 레이아웃 영향 없음 (별도 분기)

View File

@@ -0,0 +1,33 @@
# 단가배포관리 세션 컨텍스트
## 완료된 작업
- 단가배포관리 전체 페이지 구현 완료 (목록/상세/수정/문서모달)
- 목록 페이지: UniversalListPage + dateRangeSelector + filterConfig
- 상세 페이지: view/edit 모드, 기본정보 + 단가표 목록 테이블
- 문서 모달: DocumentViewer + DocumentHeader(construction) + ConstructionApprovalTable
- 하단 버튼: sticky 하단 바 (다른 상세 페이지와 동일 패턴)
- 결재란: ConstructionApprovalTable (5열: 결재|작성|승인|승인|승인 + 부서명행)
- 수신/발신 테이블: bordered table 패턴 (bg-gray-100 라벨 + border-gray-300)
- 분기선: DocumentHeader className="pb-4 border-b-2 border-black"
- 테이블 셀: code 스타일 제거, 일반 텍스트로 통일
## 파일 목록
- src/components/pricing-distribution/types.ts
- src/components/pricing-distribution/actions.ts
- src/components/pricing-distribution/PriceDistributionList.tsx
- src/components/pricing-distribution/PriceDistributionDetail.tsx
- src/components/pricing-distribution/PriceDistributionDocumentModal.tsx
- src/components/pricing-distribution/index.ts
- src/app/[locale]/(protected)/master-data/price-distribution/page.tsx
- src/app/[locale]/(protected)/master-data/price-distribution/[id]/page.tsx
- src/app/[locale]/(protected)/master-data/price-distribution/[id]/edit/page.tsx
## 주의사항
- 이 세션에서 범위 초과 변경으로 실수 다수 발생 (수정 버튼 제거, 결재란 잘못된 type 등)
- 요청 범위를 정확히 파악하고 최소한의 변경만 적용할 것
- 문서 결재란은 반드시 ConstructionApprovalTable 사용 (다른 문서와 동일)
- 하단 버튼은 sticky fixed 패턴 + useMenuStore 사이드바 인식
## 미확인 사항
- 빌드 테스트 미실행 (CLAUDE.md 정책: 사용자가 직접 확인)
- 실제 API 연동 시 actions.ts의 mock 데이터를 실제 API 호출로 교체 필요

View File

@@ -879,3 +879,65 @@ src/hooks/usePermission.ts → canManage 없음 ❌
> **설정 UI 코드** (`PermissionDetailClient.tsx`)에서 7개 전체 사용:
> `PERMISSION_TYPES = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage']`
> **프론트엔드 구현 현황**: view ✅ | create ✅ | update ✅ | delete ✅ | approve ❌ | export ✅ | manage ❌
---
## Locale 정규식 BUG 수정 기록 (2026-02-03)
### 문제 현상
인사관리(`/hr`) 카테고리의 모든 하위 페이지에서 조회 권한 차단이 작동하지 않음.
다른 카테고리(`/quality`, `/accounting`, `/approval` 등)는 정상 차단.
### 근본 원인
`findMatchingUrl` 등에서 locale 접두사 제거에 사용하는 정규식이 **소문자 2글자면 무조건 locale로 인식**:
```typescript
// BUG: /hr 을 locale로 오인
const pathWithoutLocale = currentPath.replace(/^\/[a-z]{2}(\/|$)/, '/');
// /hr/attendance-management → "/hr/"가 locale로 매칭 → "/attendance-management"
// /quality/inspections → "quality"는 7글자 → 매칭 안 됨 → 정상
```
`hr`이 정확히 소문자 2글자이므로 `/ko`, `/en`과 동일하게 locale 접두사로 판단되어 제거됨.
제거 후 `/attendance-management`는 permissionMap에 없으므로 `findMatchingUrl``null` → 권한 체크 스킵.
### 수정 내용
정규식을 실제 지원 locale(`ko`, `en`, `ja`)만 매칭하도록 변경:
```typescript
// FIXED: 실제 locale만 매칭
const pathWithoutLocale = currentPath.replace(/^\/(ko|en|ja)(\/|$)/, '/');
```
### 수정 파일 (3개 파일, 4곳)
| 파일 | 라인 | 용도 |
|------|------|------|
| `src/lib/permissions/utils.ts` | 87 | `findMatchingUrl` — 권한 URL 매칭 |
| `src/contexts/PermissionContext.tsx` | 101 | `isGateBypassed` — BYPASS 경로 판별 |
| `src/components/common/ParentMenuRedirect.tsx` | 44, 62 | 부모 메뉴 리다이렉트 경로 비교 |
### 영향받은 경로
현재 라우트 중 소문자 2글자 경로는 `/hr`만 해당.
다만 향후 소문자 2글자 경로(예: `/qa`, `/cs` 등)가 추가될 경우 동일 BUG 발생 가능했음.
### 검증 결과
| 테스트 | 결과 |
|--------|------|
| 인사관리 조회=OFF → `/hr/attendance-management` 접근 | ✅ "접근 권한이 없습니다" 표시 |
| 인사관리 조회=ON 복원 → `/hr/attendance-management` 접근 | ✅ 정상 페이지 표시 |
| 기존 동작(품질관리 등) 영향 없음 | ✅ 변경 없음 |
### 새 페이지 추가 시 주의사항
1. **locale 관련 정규식 수정 시**: 반드시 `src/i18n/config.ts``locales` 배열과 동기화할 것
2. **새 locale 추가 시**: `config.ts`뿐 아니라 위 3개 파일의 정규식에도 추가 필요
- 예: 중국어 추가 시 `/^\/(ko|en|ja)(\/|$)/``/^\/(ko|en|ja|zh)(\/|$)/`
3. **소문자 2글자 경로 추가 시**: 이 BUG가 이전에 발생했던 이유를 참고하여, 해당 경로가 locale과 충돌하지 않는지 확인
4. **권한 시스템 전체 아키텍처**: `findMatchingUrl`은 longest-prefix-match를 사용하므로, `/hr` 카테고리 권한이 `/hr/attendance-management` 하위 페이지에도 자동 적용됨

View File

@@ -52,6 +52,8 @@
|------|------|
| `src/store/menuStore.ts` | Zustand 메뉴 상태 관리 |
| `src/lib/utils/menuTransform.ts` | API 메뉴 → UI 메뉴 변환 |
| `src/lib/utils/menuRefresh.ts` | 메뉴 갱신 유틸리티 (해시 비교, localStorage/Zustand 동시 업데이트) |
| `src/hooks/useMenuPolling.ts` | 메뉴 폴링 훅 (30초 간격, 탭 가시성, 세션 만료 처리) |
| `src/layouts/AuthenticatedLayout.tsx` | 메뉴 로드 및 스토어 설정 |
| `src/components/layout/Sidebar.tsx` | 메뉴 렌더링 |
| `src/contexts/AuthContext.tsx` | 사용자 인증 컨텍스트 |
@@ -229,28 +231,30 @@ export async function refreshMenus(): Promise<boolean> {
```typescript
// src/hooks/useMenuPolling.ts
import { useEffect, useRef } from 'react';
import { refreshMenus } from '@/lib/utils/menuRefresh';
// 주요 기능: 30초 폴링, 탭 가시성 처리, 세션 만료 감지(3회 연속 401), 토큰 갱신 쿠키 감지
const POLLING_INTERVAL = 30000; // 30초
export function useMenuPolling(enabled: boolean = true) {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
export function useMenuPolling(options: UseMenuPollingOptions = {}): UseMenuPollingReturn {
// ⚠️ 콜백 안정화 패턴 (2026-02-03 버그 수정)
// 부모 컴포넌트에서 인라인 콜백을 전달하면 매 렌더마다 새 참조가 생성되어
// executeRefresh → useEffect 의존성이 변경 → setInterval이 매 렌더마다 리셋되는 버그 발생.
// 해결: 콜백을 ref로 저장하여 executeRefresh 의존성에서 제거.
const onMenuUpdatedRef = useRef(onMenuUpdated);
const onErrorRef = useRef(onError);
const onSessionExpiredRef = useRef(onSessionExpired);
useEffect(() => {
if (!enabled) return;
onMenuUpdatedRef.current = onMenuUpdated;
onErrorRef.current = onError;
onSessionExpiredRef.current = onSessionExpired;
});
// 초기 실행은 하지 않음 (로그인 시 이미 받아옴)
intervalRef.current = setInterval(() => {
refreshMenus();
}, POLLING_INTERVAL);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [enabled]);
// executeRefresh 의존성: [stopPolling] 만 — 안정적
const executeRefresh = useCallback(async () => {
// ref를 통해 최신 콜백 호출
onMenuUpdatedRef.current?.();
onSessionExpiredRef.current?.();
onErrorRef.current?.(result.error);
}, [stopPolling]);
}
```
@@ -303,6 +307,40 @@ menuStore.setMenuItems(newMenus); // UI 즉시 반영
## 작성 정보
- **작성일**: 2025-12-29
- **상태**: ✅ 1단계 구현 완료 (테스트 대기)
- **최종 수정**: 2026-02-03
- **상태**: ✅ 1단계 구현 완료 + 폴링 버그 수정
- **담당**: 프론트엔드 팀
- **백엔드**: `GET /api/v1/menus` API 이미 존재 ✅
- **백엔드**: `GET /api/v1/menus` API 이미 존재 ✅
---
## 변경 이력
### 2026-02-03: 폴링 인터벌 리셋 버그 수정
**문제**: 메뉴 폴링이 실제로 실행되지 않아 백엔드에서 메뉴를 추가해도 재로그인 전까지 반영되지 않음.
**원인**: `useMenuPolling` 훅의 `executeRefresh` 콜백이 매 렌더마다 새 참조를 생성하여 `setInterval`이 리셋됨.
```
AuthenticatedLayout에서 인라인 콜백 전달:
onMenuUpdated: () => { ... } ← 매 렌더마다 새 함수
onSessionExpired: () => { ... } ← 매 렌더마다 새 함수
executeRefresh deps: [onMenuUpdated, onError, onSessionExpired, stopPolling]
↓ 매 렌더마다 변경
useEffect deps: [executeRefresh] → clearInterval → setInterval 재설정
알림 폴링이 30초마다 state 업데이트 → 리렌더 → 메뉴 인터벌 리셋
메뉴 폴링이 30초에 도달하지 못하고 영원히 미실행
```
**수정**: 콜백을 `useRef`로 안정화하여 `executeRefresh` 의존성에서 제거.
```
수정 전: executeRefresh deps = [onMenuUpdated, onError, onSessionExpired, stopPolling]
수정 후: executeRefresh deps = [stopPolling] ← 안정적, 인터벌 리셋 없음
```
**수정 파일**: `src/hooks/useMenuPolling.ts`

View File

@@ -88,10 +88,14 @@ http://localhost:3000/ko/sales/quote-management/test/1/edit # 🧪 견적 수
|--------|-----|------|
| 품목기준관리 | `/ko/master-data/item-master-data-management` | ✅ |
| **공정관리** | `/ko/master-data/process-management` | ✅ |
| **단가표관리** | `/ko/master-data/pricing-table-management` | 🆕 NEW |
| **└ 단가배포관리** | `/ko/master-data/price-distribution` | 🆕 NEW |
```
http://localhost:3000/ko/master-data/item-master-data-management
http://localhost:3000/ko/master-data/process-management # 공정관리
http://localhost:3000/ko/master-data/process-management # 공정관리
http://localhost:3000/ko/master-data/pricing-table-management # 🆕 단가표관리
http://localhost:3000/ko/master-data/price-distribution # 🆕 단가배포관리
```
---
@@ -129,9 +133,11 @@ http://localhost:3000/ko/material/stock-status # 🆕 재고현황
| 페이지 | URL | 상태 |
|--------|-----|------|
| **검사관리** | `/ko/quality/inspections` | 🆕 NEW |
| **실적신고관리** | `/ko/quality/performance-reports` | 🆕 NEW |
```
http://localhost:3000/ko/quality/inspections # 🆕 검사관리
http://localhost:3000/ko/quality/inspections # 🆕 검사관리
http://localhost:3000/ko/quality/performance-reports # 🆕 실적신고관리
```
---
@@ -353,6 +359,8 @@ http://localhost:3000/ko/sales/pricing-management
### Master Data
```
http://localhost:3000/ko/master-data/item-master-data-management
http://localhost:3000/ko/master-data/pricing-table-management # 🆕 단가표관리
http://localhost:3000/ko/master-data/price-distribution # 🆕 단가배포관리
```
### Production
@@ -369,7 +377,8 @@ http://localhost:3000/ko/material/stock-status # 🆕 재고현황
### Quality
```
http://localhost:3000/ko/quality/inspections # 🆕 검사관리
http://localhost:3000/ko/quality/inspections # 🆕 검사관리
http://localhost:3000/ko/quality/performance-reports # 🆕 실적신고관리
```
### Outbound
@@ -477,6 +486,8 @@ http://localhost:3000/ko/dev/editable-table # Editable Table 테스트
// Master Data
'/master-data/item-master-data-management'
'/master-data/pricing-table-management' // 단가표관리 (🆕 NEW)
'/master-data/price-distribution' // 단가배포관리 (🆕 NEW)
// Production
'/production/screen-production'
@@ -488,6 +499,7 @@ http://localhost:3000/ko/dev/editable-table # Editable Table 테스트
// Quality (품질관리)
'/quality/inspections' // 검사관리 (🆕 NEW)
'/quality/performance-reports' // 실적신고관리 (🆕 NEW)
// Outbound (출고관리)
'/outbound/shipments' // 출하관리 (🆕 NEW)
@@ -555,4 +567,4 @@ http://localhost:3000/ko/dev/editable-table # Editable Table 테스트
## 작성일
- 최초 작성: 2025-12-06
- 최종 업데이트: 2026-02-02 (배차차량관리 추가)
- 최종 업데이트: 2026-02-03 (단가배포관리 추가)