---
name: content-db-engineer
description: "지오정보기술 홈페이지(zioinfo-web) 정적 콘텐츠를 DB로 전환하는 구현 스킬. FAQ, 파트너사, 레퍼런스, CEO인사말, 조직도, KPI통계, 솔루션 소개 등 하드코딩 데이터를 Spring Boot JPA Entity + React API 훅으로 전환한다. 다음 상황에서 반드시 사용: (1) '홈페이지 XXX를 DB로 관리', 'FAQ DB화', '파트너사 관리자 추가' 요청; (2) 신규 Entity/Repository/Controller 구현; (3) 프론트 하드코딩 → API 연동 전환; (4) DataInitializer 초기 데이터 시딩; (5) 다시 실행, 업데이트, 수정, 보완 요청."
---

# 홈페이지 콘텐츠 DB 전환 스킬

## 기술 스택

| 레이어 | 기술 |
|--------|------|
| Backend | Spring Boot 3.2.5 + JPA (Hibernate 6) |
| DB | H2 (dev) / 서버 내장 |
| Frontend | React 18 + Vite |
| 인증 | JWT Bearer (관리자) / 없음 (공개 API) |

## 표준 구현 순서

```
1. JPA Entity → model/ 디렉토리
2. JpaRepository → repository/ 디렉토리
3. ApiController → GET /api/{resource} (공개)
4. AdminController → CRUD /api/admin/{resource} (JWT 필요)
5. DataInitializer → initXxx() 메서드 추가
6. React 훅 → useXxx() in Company.jsx 또는 해당 페이지
7. 컴포넌트 → API 데이터 사용, 정적 배열 제거
```

## 파일 위치 규칙

```
backend/src/main/java/kr/co/zioinfo/web/
├── model/          ← Entity (CompanyHistory.java 참조)
├── repository/     ← JpaRepository
├── controller/
│   ├── ApiController.java    ← 공개 GET 추가
│   └── AdminController.java  ← CRUD 추가
└── config/
    └── DataInitializer.java  ← initXxx() 추가

frontend/src/pages/
├── {Page}.jsx      ← useXxx() 훅 + API 데이터 사용
└── admin/
    └── Admin{Name}.jsx  ← 관리자 CRUD 페이지
```

## 우선순위별 DB화 항목

### HIGH — 즉시 구현

| 항목 | 파일 | 엔티티 | 공개 API |
|------|------|--------|---------|
| 구축 레퍼런스 | Business.jsx | `Reference` (tb_reference) | GET /api/references |
| FAQ | Support.jsx | `Faq` (tb_faq) | GET /api/faqs |
| 파트너사 | Business.jsx | `Partner` (tb_partner) | GET /api/partners |
| KPI 통계 | Home.jsx | `KpiStat` (tb_kpi_stat) | GET /api/stats |

### MEDIUM — 단계적 구현

| 항목 | 파일 | 엔티티 | 공개 API |
|------|------|--------|---------|
| CEO 인사말 | Company.jsx | `CeoGreeting` (tb_ceo_greeting) | GET /api/ceo-greeting |
| 핵심 가치 | Company.jsx | `CoreValue` (tb_core_value) | GET /api/core-values |
| 조직도 | Company.jsx | `OrgDept` (tb_org_dept) | GET /api/org-depts |
| 솔루션 설명 | SolutionPage.jsx | `SolutionFeature` (tb_solution_feature) | GET /api/solutions/{type}/features |

## Entity 공통 필드

```java
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private boolean visible = true;
private int sortOrder = 0;
@CreatedDate private LocalDateTime createdAt;
@LastModifiedDate private LocalDateTime updatedAt;
```

## 프론트엔드 훅 패턴

```jsx
function useXxx(FALLBACK = []) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    fetch('/api/xxx')
      .then(r => r.json())
      .then(d => setData(d.length > 0 ? d : FALLBACK))
      .catch(() => setData(FALLBACK))
      .finally(() => setLoading(false));
  }, []);
  return { data, loading };
}
```

## AdminController 패턴

```java
// 목록
@GetMapping("/admin/{resource}")
public List<Entity> list() { return repo.findAllByOrderBySortOrderAsc(); }

// 생성
@PostMapping("/admin/{resource}")
public Entity create(@RequestBody Entity body) {
    body.setId(null); return repo.save(body);
}

// 수정
@PutMapping("/admin/{resource}/{id}")
public ResponseEntity<Entity> update(@PathVariable Long id, @RequestBody Entity body) {
    return repo.findById(id).map(e -> {
        // 필드 복사
        return ResponseEntity.ok(repo.save(e));
    }).orElse(ResponseEntity.notFound().build());
}

// 삭제
@DeleteMapping("/admin/{resource}/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
    repo.deleteById(id); return ResponseEntity.noContent().build();
}
```
