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>
123 lines
4.2 KiB
Markdown
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();
|
|
}
|
|
```
|