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:
byeongcheolryu
2025-12-20 14:33:11 +09:00
parent c6b605200d
commit d7f491fa84
50 changed files with 666 additions and 246 deletions

View File

@@ -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`