zioinfo-web/.claude/skills/content-db-engineer/SKILL.md
DESKTOP-TKLFCPRython 06568073f2 feat(harness): homepage CMS harness for DB content management
Agents:
- content-analyst: scan static content, design JPA entities
- content-db-engineer: implement Entity/Repo/Controller/Hook
- admin-ui-builder: implement AdminXxx.jsx + sidebar + routes

Skills:
- homepage-cms-orchestrator: E2E pipeline orchestrator
- content-db-engineer: Spring Boot + React implementation guide
- admin-ui-builder: AdminHistory.jsx pattern reference

CLAUDE.md: homepage project context + harness pointer

Next DB targets: Reference, FAQ, Partner, KpiStat, CeoGreeting, OrgDept

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:02:16 +09:00

123 lines
4.2 KiB
Markdown

---
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();
}
```