refactor: 로딩 스피너 표준화 및 프로젝트 헬스 개선
- LoadingSpinner 컴포넌트 5가지 변형 구현 - LoadingSpinner (인라인/버튼용) - ContentLoadingSpinner (상세/수정 페이지) - PageLoadingSpinner (페이지 전환) - TableLoadingSpinner (테이블/리스트) - ButtonSpinner (버튼 내부) - 18개+ 페이지 로딩 UI 표준화 - HR 페이지 (사원, 휴가, 부서, 급여, 근태) - 영업 페이지 (견적, 거래처) - 게시판, 팝업관리, 품목기준정보 - API 키 보안 개선 (NEXT_PUBLIC_API_KEY → API_KEY) - Textarea 다크모드 스타일 개선 - DropdownField Radix UI Select 버그 수정 (key prop) - 프로젝트 헬스 개선 계획서 문서화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
# 프로젝트 헬스 개선 계획서
|
||||
|
||||
> 작성일: 2025-12-19
|
||||
> 최종 업데이트: 2025-12-20
|
||||
> 목적: 프로젝트 구조, 성능, 안정성 개선
|
||||
|
||||
---
|
||||
|
||||
## 현황 요약
|
||||
|
||||
| 영역 | 상태 | 핵심 이슈 |
|
||||
|------|------|----------|
|
||||
| 빌드 설정 | ✅ 해결됨 | ~~98개 타입 에러 무시~~ → 0개, ~~API 키 노출~~ → 서버 사이드 이동 |
|
||||
| 상태관리 | 🟡 개선 필요 | ItemMasterContext 과부하 (13개 상태) |
|
||||
| Next.js 활용 | 🔴 심각 | 259개 'use client', Server Component 미활용 |
|
||||
| 디자인 일관성 | ✅ 완료 | ~~다크모드 일부 미완성~~ → 다크모드 스타일 완성 (2025-12-20) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 긴급 (이번 주)
|
||||
|
||||
### 1.1 TypeScript 에러 해결 + ignoreBuildErrors 제거
|
||||
|
||||
**현재 상태:**
|
||||
```typescript
|
||||
// next.config.ts
|
||||
typescript: { ignoreBuildErrors: true } // 98개 에러 숨김
|
||||
eslint: { ignoreDuringBuilds: true }
|
||||
```
|
||||
|
||||
**작업 내용:**
|
||||
|
||||
#### Step 1: 타입 에러 카테고리 분류
|
||||
| 카테고리 | 개수 | 예시 |
|
||||
|---------|------|------|
|
||||
| 모델 타입 불일치 | ~26개 | Employee에 `concurrentPosition` 없음 |
|
||||
| Props 미스매치 | ~35개 | IntegratedListTemplateV2 props 변경 |
|
||||
| 배열 타입 불일치 | ~9개 | PricingListItem 타입 정의 |
|
||||
| 기타 | ~28개 | - |
|
||||
|
||||
#### Step 2: 수정 순서
|
||||
1. [ ] `src/types/` 폴더의 모델 타입 정의 업데이트
|
||||
2. [ ] `IntegratedListTemplateV2` Props 인터페이스 정리
|
||||
3. [ ] 페이지별 타입 에러 수정
|
||||
4. [ ] `ignoreBuildErrors: false` 변경
|
||||
5. [ ] `npm run build` 성공 확인
|
||||
|
||||
**예상 소요:** 2-3시간
|
||||
|
||||
**위험도:** 🔴 높음 (빌드 실패 가능)
|
||||
|
||||
---
|
||||
|
||||
### 1.2 API 키 서버 사이드 이동
|
||||
|
||||
**현재 상태:**
|
||||
```env
|
||||
# .env.local
|
||||
NEXT_PUBLIC_API_KEY=42Jfwc6EaR... # 브라우저에서 노출됨!
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=... # 브라우저에서 노출됨!
|
||||
```
|
||||
|
||||
**문제점:**
|
||||
- `NEXT_PUBLIC_` 접두사 → 클라이언트 번들에 포함
|
||||
- 브라우저 개발자도구에서 확인 가능
|
||||
- API 남용/해킹 위험
|
||||
|
||||
**작업 내용:**
|
||||
|
||||
#### Step 1: 환경변수 이름 변경
|
||||
```env
|
||||
# .env.local (수정 후)
|
||||
API_KEY=42Jfwc6EaR... # 서버만 접근
|
||||
GOOGLE_MAPS_API_KEY=AIzaSyAS3bA... # 서버만 접근
|
||||
|
||||
# 클라이언트에서 필요한 공개 정보만
|
||||
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
|
||||
```
|
||||
|
||||
#### Step 2: 서버 사이드 프록시 확인
|
||||
```typescript
|
||||
// src/app/api/proxy/[...path]/route.ts
|
||||
// 이미 구현됨 - API_KEY를 서버에서 주입
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.API_KEY}`, // 서버에서만 접근
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Step 3: Google Maps 처리
|
||||
```typescript
|
||||
// 옵션 A: 서버 사이드 렌더링
|
||||
// 옵션 B: API 라우트로 프록시
|
||||
// 옵션 C: Maps Embed API 사용 (키 제한 설정)
|
||||
```
|
||||
|
||||
**예상 소요:** 30분-1시간
|
||||
|
||||
**위험도:** 🟡 중간 (dev 서버 재시작 필요)
|
||||
|
||||
---
|
||||
|
||||
### 1.3 ThemeContext SSR 수정
|
||||
|
||||
**현재 상태:**
|
||||
```typescript
|
||||
// src/contexts/ThemeContext.tsx
|
||||
const [theme, setThemeState] = useState<Theme>("light");
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem("theme"); // SSR에서 에러 가능
|
||||
if (savedTheme) {
|
||||
setThemeState(savedTheme);
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
**문제점:**
|
||||
- 서버에서 `localStorage` 접근 시 에러
|
||||
- Hydration mismatch 발생 가능
|
||||
|
||||
**작업 내용:**
|
||||
|
||||
#### 수정 코드
|
||||
```typescript
|
||||
// src/contexts/ThemeContext.tsx (수정 후)
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
// SSR 안전 체크
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
return (savedTheme as Theme) || 'light';
|
||||
});
|
||||
|
||||
// 또는 useEffect 패턴 유지 (더 안전)
|
||||
const [theme, setThemeState] = useState<Theme>('light');
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsHydrated(true);
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
setThemeState(savedTheme as Theme);
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
**예상 소요:** 15분
|
||||
|
||||
**위험도:** 🟢 낮음 (HMR 즉시 반영)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 단기 (2주)
|
||||
|
||||
### 2.1 ItemMasterContext 분할
|
||||
|
||||
**현재 상태:**
|
||||
```
|
||||
ItemMasterContext
|
||||
├── 품목 데이터 (3개 상태)
|
||||
├── 기준정보 (7개 상태)
|
||||
├── 폼 구조 (4개 상태)
|
||||
└── 50개+ 메서드
|
||||
|
||||
→ ANY 상태 변경 시 전체 리렌더링
|
||||
```
|
||||
|
||||
**개선 방향:**
|
||||
```
|
||||
ItemMasterDataContext → 품목 기본 데이터
|
||||
ItemFormContext → 페이지/섹션/필드 구조
|
||||
ItemLookupContext → 단위/재질/처리방식 등 기준정보
|
||||
```
|
||||
|
||||
**작업 내용:**
|
||||
1. [ ] Context 분할 설계
|
||||
2. [ ] 각 Context별 Provider 구현
|
||||
3. [ ] 기존 useItemMaster → 새 Context hooks로 마이그레이션
|
||||
4. [ ] 테스트
|
||||
|
||||
**예상 소요:** 1-2일
|
||||
|
||||
**위험도:** 🟡 중간 (기존 코드 변경 필요)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 IntegratedListTemplate → Zustand Store
|
||||
|
||||
**현재 상태:**
|
||||
```typescript
|
||||
// 20개+ props 전달
|
||||
<IntegratedListTemplateV2
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
currentPage={currentPage}
|
||||
onPageChange={setCurrentPage}
|
||||
selectedItems={selectedItems}
|
||||
onSelectionChange={setSelectedItems}
|
||||
// ... 더 많은 props
|
||||
/>
|
||||
```
|
||||
|
||||
**개선 방향:**
|
||||
```typescript
|
||||
// Zustand store
|
||||
const useListStore = create((set) => ({
|
||||
// 페이지네이션
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
setPage: (page) => set({ currentPage: page }),
|
||||
|
||||
// 필터/검색
|
||||
searchValue: '',
|
||||
filters: {},
|
||||
setSearch: (value) => set({ searchValue: value }),
|
||||
|
||||
// 선택
|
||||
selectedIds: new Set(),
|
||||
toggleSelection: (id) => set((state) => { /* ... */ }),
|
||||
}));
|
||||
|
||||
// 컴포넌트에서 사용
|
||||
function MyListPage() {
|
||||
const { currentPage, setPage } = useListStore();
|
||||
return <IntegratedListTemplateV2 />; // props 최소화
|
||||
}
|
||||
```
|
||||
|
||||
**작업 내용:**
|
||||
1. [ ] `src/store/listStore.ts` 생성
|
||||
2. [ ] 공통 리스트 상태 추출
|
||||
3. [ ] 페이지별 점진적 마이그레이션
|
||||
4. [ ] IntegratedListTemplateV2 리팩토링
|
||||
|
||||
**예상 소요:** 2-3일
|
||||
|
||||
**위험도:** 🟡 중간
|
||||
|
||||
---
|
||||
|
||||
### 2.3 다크모드 스타일 완성
|
||||
|
||||
**현재 상태:**
|
||||
```typescript
|
||||
// Button - 일부 variant만 dark: 정의
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground" // dark: 없음
|
||||
outline: "border-input bg-background" // dark: 없음
|
||||
```
|
||||
|
||||
**작업 내용:**
|
||||
1. [ ] 모든 UI 컴포넌트 다크모드 스타일 점검
|
||||
2. [ ] Button, Select, Input 등 주요 컴포넌트 수정
|
||||
3. [ ] 색상 대비 검증 (WCAG AA 기준)
|
||||
4. [ ] 다크모드 테스트
|
||||
|
||||
**예상 소요:** 1일
|
||||
|
||||
**위험도:** 🟢 낮음
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 중기 (1개월)
|
||||
|
||||
### 3.1 주요 페이지 Server Component 전환
|
||||
|
||||
**현재 상태:**
|
||||
- 259개 'use client' 컴포넌트
|
||||
- 모든 데이터 페칭: useEffect 내 클라이언트 fetch
|
||||
- 초기 로딩 지연
|
||||
|
||||
**개선 방향:**
|
||||
```typescript
|
||||
// Before (Client Component)
|
||||
'use client';
|
||||
export default function ItemPage() {
|
||||
const [items, setItems] = useState([]);
|
||||
useEffect(() => {
|
||||
fetch('/api/items').then(r => r.json()).then(setItems);
|
||||
}, []);
|
||||
return <ItemList items={items} />;
|
||||
}
|
||||
|
||||
// After (Server Component)
|
||||
export default async function ItemPage() {
|
||||
const items = await fetch('/api/items').then(r => r.json());
|
||||
return <ItemList items={items} />; // 클라이언트로 props 전달
|
||||
}
|
||||
```
|
||||
|
||||
**마이그레이션 우선순위:**
|
||||
1. [ ] 정적 페이지 (설정, 정보 페이지)
|
||||
2. [ ] 리스트 페이지 (items, employees)
|
||||
3. [ ] 상세 페이지
|
||||
|
||||
**예상 소요:** 1-2주
|
||||
|
||||
**위험도:** 🟡 중간
|
||||
|
||||
---
|
||||
|
||||
### 3.2 캐싱 전략 수립
|
||||
|
||||
**현재 상태:**
|
||||
```typescript
|
||||
cache: 'no-store' // 모든 fetch에 적용 → 성능 저하
|
||||
```
|
||||
|
||||
**개선 방향:**
|
||||
| 데이터 유형 | 캐싱 전략 | TTL |
|
||||
|------------|----------|-----|
|
||||
| 정적 데이터 (카테고리, 단위) | `force-cache` | 1시간 |
|
||||
| 사용자 데이터 | `no-store` | - |
|
||||
| 리스트 데이터 | `revalidate: 60` | 1분 |
|
||||
| 상세 데이터 | `revalidate: 300` | 5분 |
|
||||
|
||||
**작업 내용:**
|
||||
1. [ ] 데이터 유형별 분류
|
||||
2. [ ] fetch 옵션 표준화
|
||||
3. [ ] revalidateTag/revalidatePath 활용
|
||||
4. [ ] 성능 측정
|
||||
|
||||
**예상 소요:** 3-5일
|
||||
|
||||
**위험도:** 🟢 낮음
|
||||
|
||||
---
|
||||
|
||||
### 3.3 TanStack Query 도입 검토
|
||||
|
||||
**도입 이점:**
|
||||
- API 상태 자동 관리 (loading, error, data)
|
||||
- 캐싱 및 백그라운드 리페치
|
||||
- 낙관적 업데이트
|
||||
- DevTools 지원
|
||||
|
||||
**도입 시 구조:**
|
||||
```typescript
|
||||
// hooks/useItems.ts
|
||||
export function useItems() {
|
||||
return useQuery({
|
||||
queryKey: ['items'],
|
||||
queryFn: () => itemApi.getAll(),
|
||||
staleTime: 5 * 60 * 1000, // 5분
|
||||
});
|
||||
}
|
||||
|
||||
// 컴포넌트에서 사용
|
||||
function ItemList() {
|
||||
const { data, isLoading, error } = useItems();
|
||||
// 자동으로 loading/error 처리
|
||||
}
|
||||
```
|
||||
|
||||
**검토 포인트:**
|
||||
- [ ] 현재 API 호출 패턴 분석
|
||||
- [ ] 도입 시 마이그레이션 범위
|
||||
- [ ] 번들 사이즈 영향 (~20KB)
|
||||
- [ ] 팀 학습 비용
|
||||
|
||||
**예상 소요:** 1-2주 (검토 + 파일럿)
|
||||
|
||||
**위험도:** 🟡 중간 (큰 변화)
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트 요약
|
||||
|
||||
### Phase 1 (긴급) ⏰ ✅ 완료 (2025-12-20)
|
||||
- [x] 1.1 타입 에러 해결 + ignoreBuildErrors 제거 ✅
|
||||
- 98개 → 0개 에러 수정
|
||||
- `npx tsc --noEmit` 성공
|
||||
- `npm run build` 성공
|
||||
- [x] 1.2 API 키 서버 사이드 이동 ✅
|
||||
- `NEXT_PUBLIC_API_KEY` → `API_KEY` 변경
|
||||
- 프록시 라우트에서 서버 사이드 주입
|
||||
- [x] 1.3 ThemeContext SSR 수정 ✅
|
||||
- `typeof window` 체크 추가
|
||||
|
||||
### Phase 2 (단기) 📅
|
||||
- [ ] 2.1 ItemMasterContext 3개로 분할
|
||||
- [ ] 2.2 IntegratedListTemplate → Zustand store
|
||||
- [x] 2.3 다크모드 스타일 완성 ✅ (2025-12-20)
|
||||
- Textarea: Input과 스타일 통일 (`dark:bg-input/30` 추가)
|
||||
- 모든 UI 컴포넌트 다크모드 지원 확인:
|
||||
- Button, Select, Input ✅ (dark: 스타일 적용됨)
|
||||
- Card, Dialog, Sheet, Popover ✅ (CSS 변수로 처리)
|
||||
- Table, DropdownMenu ✅ (CSS 변수 + dark: 스타일)
|
||||
- Badge, Checkbox, RadioGroup, Switch ✅ (dark: 스타일 적용됨)
|
||||
- Alert, Tabs ✅ (CSS 변수 + dark: 스타일)
|
||||
- [x] 2.4 로딩 스피너 표준화 ✅ (2025-12-20)
|
||||
- `loading-spinner.tsx` 5가지 변형 컴포넌트 구현:
|
||||
- `LoadingSpinner`: 인라인/버튼용 (xs, sm, md, lg 사이즈)
|
||||
- `ContentLoadingSpinner`: 상세/수정 페이지용 (min-h-[400px])
|
||||
- `PageLoadingSpinner`: 페이지 전환용 (min-h-[calc(100vh-200px)])
|
||||
- `TableLoadingSpinner`: 테이블/리스트용 (py-16)
|
||||
- `ButtonSpinner`: 버튼 내부 스피너
|
||||
- 18개+ 페이지 표준화 적용:
|
||||
- HR 페이지 (사원, 휴가, 부서, 급여, 근태관리)
|
||||
- 품목기준정보관리, 게시판, 팝업관리
|
||||
- 견적관리 상세/수정
|
||||
- 빌드 테스트 성공 (231 pages)
|
||||
|
||||
### Phase 3 (중기) 📆
|
||||
- [ ] 3.1 주요 페이지 Server Component 전환
|
||||
- [ ] 3.2 캐싱 전략 수립
|
||||
- [ ] 3.3 TanStack Query 도입 검토
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- 분석 리포트: 2025-12-19 프로젝트 헬스체크
|
||||
- 관련 문서:
|
||||
- `claudedocs/guides/[GUIDE-2025-12-16] options-vs-flattened-data.md`
|
||||
- `claudedocs/guides/[PLAN-2025-12-19] page-layout-standardization.md`
|
||||
Reference in New Issue
Block a user