feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가
자재관리: - 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장 - 재고현황 컴포넌트 리팩토링 출고관리: - 출하관리 생성/수정/목록/상세 개선 - 차량배차관리 상세/수정/목록 기능 보강 생산관리: - 작업지시서 WIP 생산 모달 신규 추가 - 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가 - 작업자화면 기능 대폭 확장 (카드/목록 개선) - 검사성적서 모달 개선 품질관리: - 실적보고서 관리 페이지 신규 추가 - 검사관리 문서/타입/목데이터 개선 단가관리: - 단가배포 페이지 및 컴포넌트 신규 추가 - 단가표 관리 페이지 및 컴포넌트 신규 추가 공통: - 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard) - 메뉴 폴링 훅 개선, 레이아웃 수정 - 모바일 줌/패닝 CSS 수정 - locale 유틸 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
67
claudedocs/[FIX-2026-02-04] mobile-zoom-panning.md
Normal file
67
claudedocs/[FIX-2026-02-04] mobile-zoom-panning.md
Normal 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 유지
|
||||
- [ ] 데스크톱 레이아웃 영향 없음 (별도 분기)
|
||||
@@ -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 호출로 교체 필요
|
||||
@@ -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` 하위 페이지에도 자동 적용됨
|
||||
|
||||
@@ -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`
|
||||
@@ -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 (단가배포관리 추가)
|
||||
|
||||
Reference in New Issue
Block a user