Compare commits

..

No commits in common. "dfc8f1484b934ea5c12043747d9c6a4cac98bb60" and "fee9b812f6eca188f29291018c6bfd5326fe9fce" have entirely different histories.

31 changed files with 210 additions and 1429 deletions

View File

@ -1,46 +0,0 @@
---
name: admin-ui-builder
description: "홈페이지 관리자 UI 구현 에이전트. content-db-engineer가 완성한 API를 바탕으로 AdminXxx.jsx 관리자 페이지를 AdminNews.jsx 패턴으로 구현하고, AdminLayout.jsx 사이드바와 App.jsx 라우트를 등록한다."
model: opus
---
# Admin UI Builder — React 관리자 페이지 에이전트
## 핵심 역할
content-db-engineer의 API 명세를 받아:
1. **AdminXxx.jsx** — CRUD 관리자 페이지 (AdminHistory.jsx 패턴)
2. **AdminLayout.jsx** — 사이드바 메뉴 항목 추가
3. **App.jsx**`/admin/{path}` 라우트 등록
## UI 패턴 (기존 코드 준수)
```jsx
// 기존 AdminHistory.jsx 패턴 그대로 적용
const authFetch = (url, opts={}) =>
fetch(url, { ...opts, headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('admin_token')}`,
...opts.headers
}});
// 기능: 목록 조회, 신규 추가 모달, 수정 모달, 삭제 확인, 공개/숨김 토글
// 토스트 알림, 검색 필터
```
## 구현 기능 목록 (모든 관리자 페이지 공통)
| 기능 | 설명 |
|------|------|
| 목록 | 테이블/카드 형태, 검색 필터 |
| 추가 | 모달 폼, 필수 필드 검증 |
| 수정 | 항목 클릭 → 수정 모달 |
| 삭제 | confirm() 확인 후 DELETE |
| 노출 토글 | 공개/숨김 즉시 전환 |
| 순서 | sortOrder 숫자 입력 |
## 팀 통신 프로토콜
- **수신**: content-db-engineer에게서 API 명세
- **발신**: homepage-cms-orchestrator에게 구현 완료 + 라우트 목록 보고

View File

@ -1,41 +0,0 @@
---
name: content-analyst
description: "홈페이지 정적 콘텐츠 분석 에이전트. 하드코딩된 텍스트·배열을 스캔하고 DB화 우선순위·영향 범위·JPA 엔티티 설계안을 도출한다."
model: opus
---
# Content Analyst — 홈페이지 콘텐츠 분석 에이전트
## 핵심 역할
`workspace/zioinfo-web/frontend/src/pages/*.jsx` 파일을 분석하여:
1. 하드코딩된 정적 콘텐츠 블록 식별
2. 변경 빈도·영향 범위 기반 DB화 우선순위 책정
3. JPA 엔티티 설계안 (테이블명·컬럼·인덱스) 도출
4. 프론트엔드 API 훅 설계안 (`useXxx()` 패턴)
## 분석 기준
| 우선순위 | 조건 |
|---------|------|
| HIGH | 월 1회 이상 변경 가능성, 비개발자가 수정해야 하는 항목 |
| MEDIUM | 분기 1회 변경, 마케팅·기획 요건에 따라 달라지는 항목 |
| LOW | 연 1회 이하, 코드 변경과 함께하는 항목 |
## 출력 형식
```
## 분석 결과: {파일명}
- 항목명: {변수명}
- 현재 위치: {파일:라인}
- 항목 수: N개
- DB화 우선순위: HIGH/MEDIUM/LOW
- 제안 엔티티: {EntityName} (tb_{table_name})
- 핵심 컬럼: id, ..., sort_order, visible, created_at
```
## 팀 통신 프로토콜
- **수신**: homepage-cms-orchestrator의 분석 요청
- **발신**: content-db-engineer에게 `{entity_design: [...]}` 전달
- **발신**: admin-ui-builder에게 `{pages_to_add: [...]}` 전달

View File

@ -1,62 +0,0 @@
---
name: content-db-engineer
description: "홈페이지 콘텐츠 DB 구현 에이전트. content-analyst 설계안을 받아 JPA Entity·Repository·ApiController·AdminController·DataInitializer를 Spring Boot 패턴으로 구현하고, 프론트엔드 useXxx() 훅으로 Company.jsx 등을 API 연동으로 전환한다."
model: opus
---
# Content DB Engineer — Spring Boot + React 구현 에이전트
## 핵심 역할
content-analyst의 설계안을 바탕으로:
1. **JPA Entity** (`workspace/zioinfo-web/backend/.../model/`)
2. **Repository** (`...repository/`)
3. **ApiController**`GET /api/{resource}` 공개 엔드포인트
4. **AdminController**`GET/POST/PUT/DELETE /api/admin/{resource}` CRUD
5. **DataInitializer** — 기존 하드코딩 데이터 초기 시딩
6. **프론트엔드 훅**`useXxx()``fetch('/api/{resource}')` + 폴백
## 구현 패턴 (기존 코드 준수)
```java
// Entity 패턴 (CompanyHistory 참조)
@Entity @Table(name = "tb_{name}")
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class {Name} {
@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
// React 훅 패턴 (useHistory 참조)
function useXxx() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/xxx').then(r=>r.json()).then(setData)
.catch(()=>setData(FALLBACK))
.finally(()=>setLoading(false));
}, []);
return { data, loading };
}
```
## 보안 원칙
- `AdminController` 엔드포인트는 JWT Bearer 토큰 필수
- `ApiController` 공개 엔드포인트는 인증 없음 (`visible=true`만 반환)
- API 응답에 내부 ID 이외 민감 정보 미포함
## 팀 통신 프로토콜
- **수신**: content-analyst에게서 `entity_design`
- **발신**: admin-ui-builder에게 완성된 API 명세 전달
- **발신**: homepage-cms-orchestrator에게 구현 완료 보고

View File

@ -1,77 +0,0 @@
---
name: admin-ui-builder
description: "지오정보기술 홈페이지 관리자(zioinfo-web) UI 구현 스킬. FAQ, 레퍼런스, 파트너사, CEO인사말, 조직도 등 새 DB 항목의 관리자 CRUD 페이지(AdminXxx.jsx)를 AdminHistory.jsx 패턴으로 구현하고 AdminLayout.jsx 사이드바와 App.jsx 라우트를 등록한다. 다음 상황에서 반드시 사용: (1) '관리자에서 XXX 관리', '관리자 페이지 추가', 'CRUD UI 추가' 요청; (2) AdminXxx.jsx 신규 생성; (3) 사이드바 메뉴 추가; (4) 다시 실행, 업데이트, 수정, 보완."
---
# 홈페이지 관리자 UI 구현 스킬
## 기본 패턴 파일
`workspace/zioinfo-web/frontend/src/pages/admin/AdminHistory.jsx` — 모든 관리자 페이지의 기준 템플릿.
## AdminXxx.jsx 필수 구성 요소
```jsx
const authFetch = (url, opts={}) =>
fetch(url, { ...opts, headers: {
'Content-Type':'application/json',
Authorization:`Bearer ${localStorage.getItem('admin_token')}`,
...opts.headers
}});
// 필수 상태
const [items, setItems] = useState([]);
const [modal, setModal] = useState(null); // null | 'create' | 'edit'
const [form, setForm] = useState(EMPTY);
const [editId, setEditId] = useState(null);
const [saving, setSaving] = useState(false);
const [toast, setToast] = useState(null);
const [search, setSearch] = useState('');
// 필수 기능: load, openCreate, openEdit, closeModal, save, del, toggleVisible
```
## AdminLayout.jsx 사이드바 추가
```js
// NAV 배열에 추가 (콘텐츠 관리 섹션 아래)
{ path: '/admin/{path}', icon: '{이모지}', label: '{메뉴명}' },
```
## App.jsx 라우트 추가
```jsx
// lazy import 추가
const Admin{Name} = lazy(() => import('./pages/admin/Admin{Name}'));
// AdminLayout Route 자식에 추가
<Route path="{path}" element={<Admin{Name} />} />
```
## 항목별 아이콘 가이드
| 항목 | 아이콘 |
|------|------|
| FAQ | ❓ |
| 레퍼런스 | 🏆 |
| 파트너사 | 🤝 |
| KPI 통계 | 📊 |
| CEO 인사말 | 👔 |
| 핵심 가치 | ⭐ |
| 조직도 | 🏢 |
| 솔루션 | 💡 |
## 공통 스타일 변수 (AdminHistory.jsx 재사용)
```jsx
const btnStyle = color => ({
padding:'7px 16px', background:color, color:'#fff',
border:'none', borderRadius:6, cursor:'pointer', fontSize:13, fontWeight:600,
});
const btnSmall = color => ({
padding:'3px 10px', background:color, color:'#fff',
border:'none', borderRadius:4, cursor:'pointer', fontSize:11, fontWeight:600,
});
const labelStyle = { display:'flex', flexDirection:'column', gap:4, fontSize:13, fontWeight:600, color:'#475569' };
const inputStyle = { padding:'8px 10px', border:'1px solid #cbd5e1', borderRadius:6, fontSize:13, outline:'none', marginTop:2 };
```

View File

@ -1,122 +0,0 @@
---
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();
}
```

View File

@ -1,146 +0,0 @@
---
name: homepage-cms-orchestrator
description: "지오정보기술 홈페이지(zioinfo-web) 콘텐츠 DB 관리 오케스트레이터. 하드코딩된 FAQ·레퍼런스·파트너사·KPI통계·CEO인사말·조직도·핵심가치·솔루션 설명을 DB로 전환하고 관리자 UI를 구현한다. 다음 상황에서 반드시 사용: (1) '홈페이지 XXX를 DB로', '관리자에서 관리 가능하게', 'FAQ DB화', '레퍼런스 CRUD 추가' 요청; (2) 신규 콘텐츠 항목 DB 전환; (3) 관리자 페이지 추가; (4) 기존 구현 수정·보완; (5) '다시 실행', '업데이트', '수정', '보완' 요청. 현재 DB 관리 중: 뉴스, 채용공고, 회사연혁, 문의, 회원."
---
# 홈페이지 CMS 오케스트레이터
**실행 모드:** 파이프라인 (서브 에이전트)
`content-analyst``content-db-engineer``admin-ui-builder` 순차 실행
---
## Phase 0: 컨텍스트 확인
요청 분류:
- **신규 DB 전환**: "XXX를 DB로 관리해줘" → Phase 1-3 전체 실행
- **관리자 UI만**: "관리자 페이지 추가해줘" → Phase 3만
- **기존 수정**: "XXX 수정해줘" → 해당 Phase만
현재 DB 관리 중인 항목 (건너뜀):
- 뉴스/공지 (`/api/admin/news`)
- 채용공고 (`/api/admin/recruit`)
- 회사연혁 (`/api/admin/history`)
- 문의 (`/api/admin/inquiries`)
- 회원 (`/api/admin/members`)
---
## Phase 1: 분석 (content-analyst)
대상 항목 파악:
```
frontend/src/pages/{Page}.jsx 읽기
→ 하드코딩 배열/객체 식별
→ DB화 우선순위 산정
→ JPA 엔티티 설계안 작성
```
**HIGH 우선순위 구현 목록:**
| 항목 | 파일 | 엔티티명 | 테이블명 | API |
|------|------|---------|---------|-----|
| 구축 레퍼런스 | Business.jsx | `Reference` | tb_reference | /api/references |
| FAQ | Support.jsx | `Faq` | tb_faq | /api/faqs |
| 파트너사 | Business.jsx | `Partner` | tb_partner | /api/partners |
| KPI 통계 | Home.jsx | `KpiStat` | tb_kpi_stat | /api/stats |
**MEDIUM 우선순위:**
| 항목 | 파일 | 엔티티명 | 테이블명 | API |
|------|------|---------|---------|-----|
| CEO 인사말 | Company.jsx | `CeoGreeting` | tb_ceo_greeting | /api/ceo-greeting |
| 핵심 가치 | Company.jsx | `CoreValue` | tb_core_value | /api/core-values |
| 조직도 부서 | Company.jsx | `OrgDept` | tb_org_dept | /api/org-depts |
| 솔루션 기능 | SolutionPage.jsx | `SolutionFeature` | tb_solution_feature | /api/solutions/{type}/features |
---
## Phase 2: 구현 (content-db-engineer)
`content-db-engineer` 스킬 참조하여:
```
1. Entity 파일 생성
2. Repository 인터페이스 생성
3. ApiController에 GET 엔드포인트 추가
4. AdminController에 CRUD 추가
5. DataInitializer에 initXxx() 추가 (기존 하드코딩 데이터 시딩)
6. 프론트 페이지에 useXxx() 훅 추가, 정적 배열 제거
```
---
## Phase 3: 관리자 UI (admin-ui-builder)
`admin-ui-builder` 스킬 참조하여:
```
1. AdminXxx.jsx 생성 (AdminHistory.jsx 패턴)
2. AdminLayout.jsx NAV 배열에 메뉴 추가
3. App.jsx lazy import + Route 추가
4. 빌드 검증 (vite build)
```
---
## Phase 4: 배포
```bash
# 1. 로컬 빌드 검증
cd workspace/zioinfo-web/frontend
node_modules/.bin/vite.cmd build --outDir C:\Temp\zioinfo-build
# 2. 서버 배포 (deploy_history.py 패턴)
python C:\GUARDiA\deploy_history.py
```
---
## 관리자 URL 목록
| 항목 | 관리자 URL |
|------|----------|
| 뉴스/공지 (기존) | /admin/news |
| 채용공고 (기존) | /admin/recruit |
| 회사연혁 (기존) | /admin/history |
| 문의 (기존) | /admin/inquiries |
| 레퍼런스 (신규) | /admin/references |
| FAQ (신규) | /admin/faqs |
| 파트너사 (신규) | /admin/partners |
| KPI 통계 (신규) | /admin/stats |
| CEO 인사말 (신규) | /admin/ceo-greeting |
| 핵심 가치 (신규) | /admin/core-values |
| 조직도 (신규) | /admin/org |
---
## 테스트 시나리오
**정상 흐름:**
1. `GET /api/faqs` → FAQ 목록 JSON 반환
2. 관리자 로그인 → `/admin/faqs` 페이지 접근
3. FAQ 추가 모달 → 저장 → 목록 갱신
4. 홈페이지 `/support/*` → API에서 FAQ 동적 로드
**에러 흐름:**
1. API 실패 → 프론트 폴백(FALLBACK 배열) 표시
2. 인증 없이 POST → 401 반환
3. 존재하지 않는 ID DELETE → 404 반환
---
## 테스트 시나리오 (should-trigger)
- "FAQ를 DB로 관리하고 싶어"
- "레퍼런스 관리자에서 추가/삭제 가능하게"
- "파트너사 목록 DB화해줘"
- "CEO 인사말 수정할 수 있게 해줘"
- "홈페이지 통계 수치 관리자에서 바꾸고 싶어"
## 테스트 시나리오 (should-NOT-trigger)
- "뉴스 작성해줘" → `/admin/news` (기존 구현)
- "채용공고 수정" → `/admin/recruit` (기존 구현)
- "홈페이지 디자인 수정" → 직접 CSS 편집
- "ITSM에 새 기능 추가" → guardia-orchestrator

View File

@ -1,45 +0,0 @@
# 지오정보기술 홈페이지 (zioinfo-web)
> Spring Boot 3.2.5 + React 18 + Vite | 서버: zioinfo.co.kr:8082
---
## 기술 스택
| 레이어 | 기술 |
|--------|------|
| Backend | Spring Boot 3.2.5 (Java 17), JPA, H2 |
| Frontend | React 18 + Vite, React Router DOM |
| 인증 | JWT (관리자) / 자체 JWT (회원) |
| 서버 | 101.79.17.164, Spring Boot 포트 8082 |
| 빌드 | `mvn clean package -DskipTests` → jar → `/opt/zioinfo/app/app.jar` |
## 배포 파이프라인
```
1. 프론트 빌드: node_modules\.bin\vite.cmd build --outDir C:\Temp\zioinfo-build
2. 백엔드 배포: python C:\GUARDiA\deploy_history.py (패턴 참조)
3. 서버 재시작: systemctl restart zioinfo
```
## DB 관리 항목 현황
| 항목 | 엔티티 | 공개 API | 관리자 UI |
|------|--------|---------|---------|
| 뉴스/공지 | News | GET /api/news | /admin/news |
| 채용공고 | Recruit | GET /api/recruit | /admin/recruit |
| 회사 연혁 | CompanyHistory | GET /api/history | /admin/history |
| 문의 | Inquiry | POST /api/inquiry | /admin/inquiries |
| 회원 | Member | - | /admin/members |
## 하네스: 홈페이지 CMS
**목표:** 홈페이지 정적 텍스트(FAQ·레퍼런스·파트너사·CEO인사말·조직도 등)를 DB로 전환하고 관리자에서 CRUD 가능하게 유지
**트리거:** 홈페이지 콘텐츠를 DB로 관리, 관리자 페이지 추가, FAQ·레퍼런스·파트너사 등 새 항목 DB화 요청 시 `homepage-cms-orchestrator` 스킬을 사용하라.
**변경 이력:**
| 날짜 | 변경 내용 | 대상 | 사유 |
|------|----------|------|------|
| 2026-05-31 | 초기 하네스 구성 | 전체 | 홈페이지 CMS 체계화 |
| 2026-05-31 | 회사 연혁 DB 전환 완료 | CompanyHistory + AdminHistory | 첫 번째 DB화 사례 |

View File

@ -15,7 +15,6 @@ public class DataInitializer implements CommandLineRunner {
private final NewsRepository newsRepo;
private final AdminUserRepository adminUserRepo;
private final RecruitRepository recruitRepo;
private final CompanyHistoryRepository historyRepo;
private final PasswordEncoder passwordEncoder;
@Override
@ -23,7 +22,6 @@ public class DataInitializer implements CommandLineRunner {
initAdmin();
initNews();
initRecruits();
initHistory();
}
private void initAdmin() {
@ -96,55 +94,4 @@ public class DataInitializer implements CommandLineRunner {
.preferred("- 공공기관 정보보호 인증 자격증 (정보처리기사 등)\n- Kubernetes 경험")
.deadline(LocalDate.of(2026, 7, 31)).headcount(1).active(true).build());
}
private void initHistory() {
if (historyRepo.count() > 0) return;
Object[][] data = {
{"2026", 0, "GUARDiA ITSM v2.0 출시 — AI ChatOps 오케스트레이션 플랫폼"},
{"2026", 1, "GS인증 1등급 신청 준비 완료 (TTA 심사 예정)"},
{"2026", 2, "공공기관 1,000개 이상 멀티테넌트 지원 목표 달성"},
{"2025", 0, "삼성전자 차세대 CRM 구축 (DB Migration / DA / 튜닝)"},
{"2025", 1, "GUARDiA ITSM v1.0 베타 서비스 개시"},
{"2025", 2, "AI 기반 인프라 자동화 특허 출원"},
{"2024", 0, "DELL 차세대 CRM 구축 — DBA 역할 수행 (엠로)"},
{"2024", 1, "소상공인컨설팅시스템 구축 (서울신용보증재단, PM)"},
{"2024", 2, "국민연금 차세대 시스템 구축 (AA)"},
{"2023", 0, "헌법재판소 포털시스템 구축 (PM)"},
{"2023", 1, "서울신용보증재단 모바일앱 구축 완료 (PM)"},
{"2022", 0, "에이텍에이피 통합유지보수관리시스템 개발 (PM)"},
{"2022", 1, "헌법재판소 통합보안관제시스템 구축 (PM)"},
{"20202021", 0, "현대백화점 HKOS 시스템 개발/구축 (PM)"},
{"20202021", 1, "서울시립대 대학행정정보시스템 성능 개선 (PL)"},
{"20202021", 2, "농협 하나로마트 ESL 시스템 구축 (PM)"},
{"20182019", 0, "이마트 정산시스템 프로젝트 (DA)"},
{"20182019", 1, "우체국금융 스마트ATM 도입 (PMO)"},
{"20182019", 2, "현대백화점 무인POS시스템 구축 (PM)"},
{"20182019", 3, "갤러리아백화점 PDA 정산시스템 (PM)"},
{"20152017", 0, "LG U+ VAN 고도화 — 승인시스템 개발 FEP/AP/BEP (AA)"},
{"20152017", 1, "한화그룹 4사 통합 HR시스템 구축 (PL)"},
{"20152017", 2, "참좋은여행 콜센터 어플리케이션 구축 (PL)"},
{"20132014", 0, "삼성전자 품질관리시스템(QWINGS) 구축 (PM)"},
{"20132014", 1, "대우증권 통합인프라시스템 (DBA)"},
{"20132014", 2, "현대캐피탈 차세대시스템 (PL)"},
{"20132014", 3, "중소기업 1357 통합콜센터 구축 (PL)"},
{"20102012", 0, "삼성전자서비스 eZone 갱신 (PL)"},
{"20102012", 1, "현대모비스 원가관리시스템 (DBA)"},
{"20102012", 2, "한국전기안전공사 전기안전포털시스템 (DBA)"},
{"20082009", 0, "국민은행 차세대 포탈 구축 (PL)"},
{"20082009", 1, "한국원자력연료 인사정보(HMS)시스템 (DBA)"},
{"20082009", 2, "한국전기안전공사 안전점검 고도화 (DBA)"},
{"2000", 0, "(주)지오정보기술 창립"},
{"2000", 1, "공공기관 IT 인프라 서비스 개시"},
};
for (Object[] row : data) {
historyRepo.save(CompanyHistory.builder()
.year((String) row[0])
.sortOrder((Integer) row[1])
.content((String) row[2])
.visible(true)
.build());
}
}
}

View File

@ -9,7 +9,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/admin")
@ -21,7 +20,6 @@ public class AdminController {
private final InquiryRepository inquiryRepo;
private final RecruitRepository recruitRepo;
private final MemberRepository memberRepo;
private final CompanyHistoryRepository historyRepo;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;
@ -57,52 +55,6 @@ public class AdminController {
return ResponseEntity.ok(stats);
}
// 연혁 관리 (CRUD)
@GetMapping("/history")
public ResponseEntity<List<CompanyHistory>> adminHistory() {
return ResponseEntity.ok(historyRepo.findAllByOrderByYearDescSortOrderAsc());
}
@PostMapping("/history")
public ResponseEntity<CompanyHistory> createHistory(@RequestBody CompanyHistory h) {
h.setId(null);
return ResponseEntity.ok(historyRepo.save(h));
}
@PutMapping("/history/{id}")
public ResponseEntity<CompanyHistory> updateHistory(
@PathVariable Long id, @RequestBody CompanyHistory body) {
return historyRepo.findById(id).map(h -> {
h.setYear(body.getYear());
h.setContent(body.getContent());
h.setSortOrder(body.getSortOrder());
h.setVisible(body.isVisible());
return ResponseEntity.ok(historyRepo.save(h));
}).orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/history/{id}")
public ResponseEntity<Void> deleteHistory(@PathVariable Long id) {
historyRepo.deleteById(id);
return ResponseEntity.noContent().build();
}
/** 연도별 그룹 조회 (프론트 미리보기용) */
@GetMapping("/history/grouped")
public ResponseEntity<List<Map<String, Object>>> adminHistoryGrouped() {
List<CompanyHistory> rows = historyRepo.findAllByOrderByYearDescSortOrderAsc();
Map<String, List<Map<String, Object>>> grouped = new LinkedHashMap<>();
for (CompanyHistory h : rows) {
grouped.computeIfAbsent(h.getYear(), k -> new ArrayList<>())
.add(Map.of("id", h.getId(), "content", h.getContent(),
"sortOrder", h.getSortOrder(), "visible", h.isVisible()));
}
List<Map<String, Object>> result = grouped.entrySet().stream()
.map(e -> Map.of("year", (Object)e.getKey(), "items", e.getValue()))
.collect(Collectors.toList());
return ResponseEntity.ok(result);
}
// 뉴스 관리
@GetMapping("/news")
public ResponseEntity<Page<News>> adminNews(

View File

@ -1,9 +1,7 @@
package kr.co.zioinfo.web.controller;
import kr.co.zioinfo.web.model.CompanyHistory;
import kr.co.zioinfo.web.model.Inquiry;
import kr.co.zioinfo.web.model.News;
import kr.co.zioinfo.web.repository.CompanyHistoryRepository;
import kr.co.zioinfo.web.repository.RecruitRepository;
import kr.co.zioinfo.web.service.InquiryService;
import kr.co.zioinfo.web.service.NewsService;
@ -22,7 +20,6 @@ public class ApiController {
private final NewsService newsService;
private final InquiryService inquiryService;
private final RecruitRepository recruitRepo;
private final CompanyHistoryRepository historyRepo;
// 회사 정보
@GetMapping("/company")
@ -43,25 +40,33 @@ public class ApiController {
return ResponseEntity.ok(info);
}
// 연혁 (DB 기반)
// 연혁
@GetMapping("/history")
public ResponseEntity<List<Map<String, Object>>> getHistory() {
List<CompanyHistory> rows = historyRepo.findByVisibleTrueOrderByYearDescSortOrderAsc();
// 연도별 그룹핑
Map<String, List<String>> grouped = new LinkedHashMap<>();
for (CompanyHistory h : rows) {
grouped.computeIfAbsent(h.getYear(), k -> new ArrayList<>()).add(h.getContent());
}
List<Map<String, Object>> result = new ArrayList<>();
for (Map.Entry<String, List<String>> e : grouped.entrySet()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("year", e.getKey());
m.put("items", e.getValue());
result.add(m);
}
return ResponseEntity.ok(result);
List<Map<String, Object>> history = new ArrayList<>();
history.add(Map.of("year", "2026", "events", List.of(
"GUARDiA ITSM v2.0 출시 (AI 자율 운영 플랫폼)",
"공공기관 AI 인프라 자동화 사업 수주"
)));
history.add(Map.of("year", "2024", "events", List.of(
"GUARDiA ITSM v1.0 개발 완료",
"관공서 레거시 인프라 자동화 특허 출원"
)));
history.add(Map.of("year", "2022", "events", List.of(
"AI 기반 ChatOps 플랫폼 연구 개발 착수",
"행정기관 SI 사업 10건 수주"
)));
history.add(Map.of("year", "2020", "events", List.of(
"창립 20주년",
"클라우드 전환 컨설팅 사업 진출"
)));
history.add(Map.of("year", "2010", "events", List.of(
"ERP·CRM 솔루션 공급 100개사 달성"
)));
history.add(Map.of("year", "2000", "events", List.of(
"(주)지오정보기술 설립"
)));
return ResponseEntity.ok(history);
}
// GUARDiA 솔루션 정보

View File

@ -1,38 +0,0 @@
package kr.co.zioinfo.web.model;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity @Table(name = "company_history")
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class CompanyHistory {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 연도 또는 기간 표시 (예: "2026", "20202021") */
@Column(nullable = false, length = 20)
private String year;
/** 항목 내용 */
@Column(nullable = false, length = 500)
private String content;
/** 같은 연도 내 순서 */
@Column(name = "sort_order")
private int sortOrder = 0;
/** 노출 여부 */
private boolean visible = true;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}

View File

@ -1,21 +0,0 @@
package kr.co.zioinfo.web.repository;
import kr.co.zioinfo.web.model.CompanyHistory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface CompanyHistoryRepository extends JpaRepository<CompanyHistory, Long> {
/** 공개 연혁만, 연도 역순 → 순서 오름차순 */
List<CompanyHistory> findByVisibleTrueOrderByYearDescSortOrderAsc();
/** 관리자용 전체 목록 */
List<CompanyHistory> findAllByOrderByYearDescSortOrderAsc();
/** 연도별 항목 수 */
long countByYear(String year);
/** 특정 연도 삭제 */
void deleteByYear(String year);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

View File

@ -29,7 +29,6 @@ const AdminInquiry = lazy(() => import('./pages/admin/AdminInquiry'));
const AdminRecruit = lazy(() => import('./pages/admin/AdminRecruit'));
const AdminSettings = lazy(() => import('./pages/admin/AdminSettings'));
const AdminMember = lazy(() => import('./pages/admin/AdminMember'));
const AdminHistory = lazy(() => import('./pages/admin/AdminHistory'));
function Loading() {
return (
@ -66,7 +65,6 @@ export default function App() {
<Route path="inquiries" element={<AdminInquiry />} />
<Route path="recruit" element={<AdminRecruit />} />
<Route path="members" element={<AdminMember />} />
<Route path="history" element={<AdminHistory />} />
<Route path="settings" element={<AdminSettings />} />
</Route>
<Route path="*" element={<Navigate to="/admin/login" replace />} />

View File

@ -48,8 +48,9 @@ export default function Footer() {
{/* 회사 정보 */}
<div className="footer-brand">
<Link to="/" className="footer-logo">
<img src="/logo_bottom.png" alt="(주)지오정보기술 로고" height="36"
onError={e => { e.target.style.display='none'; e.target.nextSibling.style.display='block'; }} />
<img src="/지오정보기술로고.png" alt="(주)지오정보기술 로고" height="36"
style={{ filter: 'brightness(0) invert(1)' }}
onError={e => { e.target.src='/ziologo.png'; e.target.onerror = () => { e.target.style.display='none'; e.target.nextSibling.style.display='block'; }; }} />
<span className="footer-logo-text" style={{display:'none'}}>
<strong>Zio</strong>Info
</span>

View File

@ -105,7 +105,7 @@ export default function Header() {
<div className="header-inner container">
{/* 로고 */}
<Link to="/" className="logo" aria-label="(주)지오정보기술 홈으로">
<img src="/zioinfo-logo.png" alt="(주)지오정보기술 로고" height="40"
<img src="/지오정보기술로고.png" alt="(주)지오정보기술 로고" height="40"
onError={e => { e.target.src='/ziologo.png'; e.target.onerror = () => { e.target.style.display='none'; e.target.nextSibling.style.display='flex'; }; }} />
<span className="logo-text" style={{display:'none'}}>
<strong>Zio</strong>Info

View File

@ -58,41 +58,21 @@ export function MemberOnly({ children, feature = '이 기능' }) {
const { isLoggedIn, loaded } = useMemberAuth();
const navigate = useNavigate();
if (!loaded) {
return (
<div style={{ minHeight: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8' }}>
<span>로딩 ...</span>
</div>
);
}
if (!loaded) return null;
if (!isLoggedIn) {
return (
<div style={{
minHeight: 260, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', padding: '40px 24px',
background: '#f8fafc', borderRadius: 12, border: '1px solid #e2e8f0',
margin: '20px 0', textAlign: 'center', gap: 12,
}}>
<div style={{ fontSize: 40 }}>🔒</div>
<div style={{ fontSize: 16, fontWeight: 700, color: '#1e293b' }}>
로그인 또는 회원가입을 하셔야 사용할 있는 기능입니다.
</div>
<div style={{ fontSize: 14, color: '#64748b' }}>
{feature} 서비스는 회원 전용입니다.
</div>
<div style={{ display: 'flex', gap: 10, marginTop: 8 }}>
<div className="member-guard" style={{ position:'relative', minHeight:120 }}>
{children}
<div className="member-guard-overlay">
<div className="member-guard-icon">🔒</div>
<div className="member-guard-text">{feature} 회원 전용입니다</div>
<div className="member-guard-sub">로그인 이용하실 있습니다</div>
<button
onClick={() => navigate('/login')}
style={{ padding: '10px 28px', background: '#1a5fd8', color: '#fff',
border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 700, fontSize: 14 }}>
로그인
</button>
<button
onClick={() => navigate('/login?tab=register')}
style={{ padding: '10px 28px', background: '#fff', color: '#1a5fd8',
border: '2px solid #1a5fd8', borderRadius: 8, cursor: 'pointer', fontWeight: 700, fontSize: 14 }}>
회원가입
style={{ marginTop:8, padding:'8px 24px', background:'#1a5fd8', color:'#fff',
border:'none', borderRadius:8, cursor:'pointer', fontWeight:600, fontSize:14 }}>
로그인 / 회원가입
</button>
</div>
</div>

View File

@ -1,82 +1,19 @@
.inner-page { padding-top: var(--header-h); }
/* Variant 스타일 page-hero */
.inner-page { padding-top: var(--header-h); }
.page-hero {
background: linear-gradient(135deg, #002752 0%, #003366 60%, #005A8C 100%);
padding: 64px 0 56px;
color: #fff;
position: relative;
overflow: hidden;
background: linear-gradient(135deg, var(--secondary), var(--primary-dark));
padding: 60px 0; color: #fff;
}
.page-hero::after {
content: '';
position: absolute;
right: -80px; top: -80px;
width: 320px; height: 320px;
background: radial-gradient(circle, rgba(0,160,200,.18) 0%, transparent 70%);
pointer-events: none;
}
.section-label {
display: inline-block;
font-size: 12px; font-weight: 700; letter-spacing: .12em;
text-transform: uppercase;
color: #00A0C8;
margin-bottom: 10px;
}
.page-hero .section-label { color: #29B8D8; opacity: .9; }
.page-hero-title {
font-size: clamp(28px, 4vw, 42px);
font-weight: 900;
letter-spacing: -0.02em;
margin: 6px 0 10px;
line-height: 1.2;
}
.page-hero p { color: rgba(255,255,255,.75); font-size: 16px; line-height: 1.7; }
/* 서브 네비게이션 Variant 스타일 */
.sub-nav {
background: #fff;
border-bottom: 1px solid #E2E8F0;
position: sticky; top: var(--header-h); z-index: 10;
box-shadow: 0 2px 8px rgba(0,51,102,.06);
}
.sub-nav .container { display: flex; gap: 0; }
.sub-nav-item {
padding: 16px 20px;
font-size: 14px; font-weight: 500;
color: #475569;
border-bottom: 2px solid transparent;
transition: all 200ms;
text-decoration: none;
}
.sub-nav-item:hover { color: #003366; background: #F8FAFC; }
.sub-nav-item.active {
color: #003366;
border-bottom-color: #00A0C8;
font-weight: 700;
}
/* 섹션 공통 */
.section { padding: 80px 0; }
.section-header { text-align: center; margin-bottom: 48px; }
.section-title {
font-size: clamp(24px, 3.5vw, 38px);
font-weight: 800;
color: #003366;
letter-spacing: -0.02em;
line-height: 1.25;
}
.section-title em { color: #00A0C8; font-style: normal; }
.section-desc { font-size: 16px; color: #475569; margin-top: 12px; line-height: 1.7; }
.page-hero-title { font-size: 40px; font-weight: 900; margin: 8px 0 12px; }
.page-hero p { color: rgba(255,255,255,.75); font-size: 16px; }
/* ── 정책/약관 페이지 ─────────────────────────────────────── */
.prose { color: var(--gray-700); line-height: 1.8; font-size: 15px; }
.prose h2 { font-size: 18px; font-weight: 700; color: var(--gray-900); margin: 32px 0 12px; border-left: 4px solid #00A0C8; padding-left: 12px; }
.prose h2 { font-size: 18px; font-weight: 700; color: var(--gray-900); margin: 32px 0 12px; border-left: 4px solid var(--accent); padding-left: 12px; }
.prose p { margin-bottom: 14px; }
.prose ul { margin: 0 0 14px 20px; }
.prose ul li { margin-bottom: 6px; }
.prose a { color: #00A0C8; }
.prose a { color: var(--accent); }
.policy-table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 14px; }
.policy-table th, .policy-table td { padding: 10px 14px; border: 1px solid #E2E8F0; }
.policy-table th { background: #F8FAFC; font-weight: 600; color: #475569; text-align: left; }
.policy-footer { margin-top: 40px; padding: 20px; background: #F8FAFC; border-radius: 10px; font-size: 13px; color: #94A3B8; line-height: 1.8; }
.policy-table th, .policy-table td { padding: 10px 14px; border: 1px solid var(--gray-200); }
.policy-table th { background: var(--gray-50); font-weight: 600; color: var(--gray-700); text-align: left; }
.policy-footer { margin-top: 40px; padding: 20px; background: var(--gray-50); border-radius: 10px; font-size: 13px; color: var(--gray-500); line-height: 1.8; }

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { Routes, Route, NavLink, useNavigate } from 'react-router-dom';
import './Common.css';
import './Company.css';
@ -48,7 +48,7 @@ function Greeting() {
<div className="ceo-photo">
<div className="ceo-avatar" style={{ overflow: 'hidden', background: 'none', padding: 0 }}>
<img
src="/zioinfo-building.png"
src="/지오정보기술사옥전경.png"
alt="(주)지오정보기술 사옥 전경"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
@ -97,25 +97,83 @@ function Greeting() {
);
}
/* ── 연혁 (API에서 로드) ── */
function useHistory() {
const [history, setHistory] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/history')
.then(r => r.json())
.then(data => setHistory(data))
.catch(() => setHistory([]))
.finally(() => setLoading(false));
}, []);
return { history, loading };
}
/* ── 연혁 (정적 폴백 — API 실패 시) ── */
const HISTORY_FALLBACK = [
/* ── 연혁 ── */
const HISTORY = [
{
year: '2026', items: [
'GUARDiA ITSM v2.0 출시 — AI ChatOps 오케스트레이션 플랫폼',
'GS인증 1등급 신청 준비 완료 (TTA 심사 예정)',
'공공기관 1,000개 이상 멀티테넌트 지원 목표 달성',
]
},
{
year: '2025', items: [
'삼성전자 차세대 CRM 구축 (DB Migration / DA / 튜닝)',
'GUARDiA ITSM v1.0 베타 서비스 개시',
'AI 기반 인프라 자동화 특허 출원',
]
},
{
year: '2024', items: [
'DELL 차세대 CRM 구축 — DBA 역할 수행 (엠로)',
'소상공인컨설팅시스템 구축 (서울신용보증재단, PM)',
'국민연금 차세대 시스템 구축 (AA)',
]
},
{
year: '2023', items: [
'헌법재판소 포털시스템 구축 (PM)',
'서울신용보증재단 모바일앱 구축 완료 (PM)',
]
},
{
year: '2022', items: [
'에이텍에이피 통합유지보수관리시스템 개발 (PM)',
'헌법재판소 통합보안관제시스템 구축 (PM)',
]
},
{
year: '20202021', items: [
'현대백화점 HKOS 시스템 개발/구축 (PM)',
'서울시립대 대학행정정보시스템 성능 개선 (PL)',
'농협 하나로마트 ESL 시스템 구축 (PM)',
]
},
{
year: '20182019', items: [
'이마트 정산시스템 프로젝트 (DA)',
'우체국금융 스마트ATM 도입 (PMO)',
'현대백화점 무인POS시스템 구축 (PM)',
'갤러리아백화점 PDA 정산시스템 (PM)',
]
},
{
year: '20152017', items: [
'LG U+ VAN 고도화 — 승인시스템 개발 FEP/AP/BEP (AA)',
'한화그룹 4사 통합 HR시스템 구축 (PL)',
'참좋은여행 콜센터 어플리케이션 구축 (PL)',
]
},
{
year: '20132014', items: [
'삼성전자 품질관리시스템(QWINGS) 구축 (PM)',
'대우증권 통합인프라시스템 (DBA)',
'현대캐피탈 차세대시스템 (PL)',
'중소기업 1357 통합콜센터 구축 (PL)',
]
},
{
year: '20102012', items: [
'삼성전자서비스 eZone 갱신 (PL)',
'현대모비스 원가관리시스템 (DBA)',
'한국전기안전공사 전기안전포털시스템 (DBA)',
]
},
{
year: '20082009', items: [
'국민은행 차세대 포탈 구축 (PL)',
'한국원자력연료 인사정보(HMS)시스템 (DBA)',
'한국전기안전공사 안전점검 고도화 (DBA)',
]
},
{
@ -125,12 +183,8 @@ const HISTORY_FALLBACK = [
]
},
];
const HISTORY = HISTORY_FALLBACK;
function History() {
const { history, loading } = useHistory();
const data = history.length > 0 ? history : HISTORY_FALLBACK;
return (
<main id="main-content" className="inner-page">
<SubNav title="연혁" />
@ -141,28 +195,22 @@ function History() {
<h2 className="section-title">20+ 성장의 역사</h2>
<p className="section-desc">2000 창립 이래 국내 주요 기관·기업과 함께 성장해 왔습니다</p>
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px', color: 'var(--gray-400)' }}>
연혁을 불러오는 ...
</div>
) : (
<div className="timeline">
{data.map((h, i) => (
<div key={i} className="timeline-row">
<div className="timeline-year">{h.year}</div>
<div className="timeline-dot" />
<div className="timeline-content">
{h.items.map((item, j) => (
<div key={j} className="timeline-item">
<span className="timeline-bullet" />
{item}
</div>
))}
</div>
<div className="timeline">
{HISTORY.map((h, i) => (
<div key={i} className="timeline-row">
<div className="timeline-year">{h.year}</div>
<div className="timeline-dot" />
<div className="timeline-content">
{h.items.map((item, j) => (
<div key={j} className="timeline-item">
<span className="timeline-bullet" />
{item}
</div>
))}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</section>
</main>
@ -266,7 +314,7 @@ function CI() {
<div className="ci-logo-showcase">
<div className="ci-logo-box light" style={{ flex: 1, minHeight: '200px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '16px' }}>
<img
src="/zioinfo-logo.png"
src="/지오정보기술로고.png"
alt="(주)지오정보기술 로고"
style={{ maxWidth: '320px', width: '100%', objectFit: 'contain' }}
/>
@ -274,7 +322,7 @@ function CI() {
</div>
<div className="ci-logo-box dark" style={{ flex: 1, minHeight: '200px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '16px' }}>
<img
src="/zioinfo-logo.png"
src="/지오정보기술로고.png"
alt="(주)지오정보기술 로고 (다크)"
style={{ maxWidth: '320px', width: '100%', objectFit: 'contain', filter: 'brightness(0) invert(1)' }}
/>

View File

@ -182,11 +182,11 @@ export default function GuardiaDetail() {
</div>
<div className="gd-screenshots">
{[
{file:'itsm_02_dashboard', caption:'통합 대시보드 — SR·SLA·AI 인사이트 (실제 화면)'},
{file:'itsm_01_login', caption:'로그인 화면 — JWT + 2FA/OTP 보안'},
{file:'04_incidents', caption:'인시던트 관리 — AI 자동 RCA'},
{file:'05_agents', caption:'AI 에이전트 — Ollama 온프레미스'},
{file:'06_license', caption:'라이선스 관리 — 에디션·체험판'},
{file:'01_dashboard', caption:'통합 대시보드 — SR·SLA·AI 인사이트'},
{file:'02_sr_list', caption:'SR 서비스 요청 — 칸반/목록 뷰'},
{file:'04_incidents', caption:'인시던트 관리 — AI 자동 RCA'},
{file:'05_agents', caption:'AI 에이전트 — Ollama 온프레미스'},
{file:'06_license', caption:'라이선스 관리 — 에디션·체험판'},
].map((s,i) => (
<div key={i} className="screenshot-card">
<img src={`/screenshots/${s.file}.png`} alt={s.caption}

View File

@ -29,70 +29,37 @@ const SLIDES = [
},
];
/* ── 핵심 사업 영역 (Variant SVG 아이콘) ─────────────────── */
/* ── 핵심 사업 영역 ──────────────────────────────────────── */
const BUSINESS = [
{
svg: (
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="10" height="10" rx="2.5" fill="#005A8C" opacity=".9"/>
<rect x="18" y="4" width="10" height="10" rx="2.5" fill="#00A0C8" opacity=".8"/>
<rect x="4" y="18" width="10" height="10" rx="2.5" fill="#00A0C8" opacity=".6"/>
<rect x="18" y="18" width="10" height="10" rx="2.5" fill="#003366" opacity=".85"/>
<path d="M14 9h4M9 14v4M23 14v4M14 23h4" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
),
icon: '🤖',
title: 'AI 자동화',
desc: 'GUARDiA ITSM 플랫폼으로 레거시 인프라 운영을 완전 자동화',
path: '/solution/guardia',
color: 'var(--primary)',
},
{
svg: (
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 3L28 9v14L16 29 4 23V9L16 3z" fill="#E8F0F8" stroke="#005A8C" strokeWidth="1.5"/>
<path d="M16 3v26M4 9l12 6 12-6" stroke="#005A8C" strokeWidth="1.5" strokeLinecap="round"/>
<circle cx="16" cy="15" r="3" fill="#00A0C8"/>
</svg>
),
icon: '🏗️',
title: 'SI 구축',
desc: '공공기관 정보화사업 시스템 통합 및 맞춤형 개발',
path: '/business/reference',
color: 'var(--accent)',
},
{
svg: (
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="5" width="26" height="22" rx="3" fill="#E8F0F8" stroke="#005A8C" strokeWidth="1.5"/>
<path d="M9 22l4-6 4 3 4-8 4 5" stroke="#00A0C8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="9" cy="22" r="2" fill="#003366"/>
<circle cx="13" cy="16" r="2" fill="#005A8C"/>
<circle cx="17" cy="19" r="2" fill="#00A0C8"/>
<circle cx="21" cy="11" r="2" fill="#005A8C"/>
<circle cx="25" cy="16" r="2" fill="#003366"/>
</svg>
),
icon: '💼',
title: 'ERP·CRM·BI',
desc: '기업 경영 효율화를 위한 통합 솔루션 패키지',
path: '/solution/erp',
color: '#10B981',
},
];
/* ── GUARDiA 핵심 기능 (SVG 아이콘) ─────────────────────── */
/* ── GUARDiA 핵심 기능 ───────────────────────────────────── */
const GUARDIA_FEATURES = [
{
svg: <svg viewBox="0 0 24 24" fill="none"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" stroke="#00A0C8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>,
label: 'ChatOps', desc: '메신저 명령으로 인프라 제어',
},
{
svg: <svg viewBox="0 0 24 24" fill="none"><rect x="2" y="3" width="20" height="14" rx="2" stroke="#00A0C8" strokeWidth="2"/><path d="M8 21h8M12 17v4" stroke="#00A0C8" strokeWidth="2" strokeLinecap="round"/><path d="M6 9h.01M9 9h6" stroke="#00A0C8" strokeWidth="2" strokeLinecap="round"/></svg>,
label: '에이전트리스', desc: 'SSH만으로 에이전트 설치 없음',
},
{
svg: <svg viewBox="0 0 24 24" fill="none"><path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z" stroke="#00A0C8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>,
label: '통합 ITSM', desc: 'SR·인시던트·변경·SLA 통합',
},
{
svg: <svg viewBox="0 0 24 24" fill="none"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="#00A0C8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>,
label: '엔터프라이즈 보안', desc: 'MFA·PAM·Zero Trust',
},
{ icon: '💬', label: 'ChatOps', desc: '메신저 명령으로 인프라 제어' },
{ icon: '🔧', label: '에이전트리스', desc: 'SSH만으로 에이전트 설치 없음' },
{ icon: '📊', label: '통합 ITSM', desc: 'SR·인시던트·변경·SLA 통합' },
{ icon: '🔒', label: '엔터프라이즈 보안', desc: 'MFA·PAM·Zero Trust' },
];
/* ── KPI 수치 ────────────────────────────────────────────── */
@ -185,43 +152,40 @@ export default function Home() {
</div>
</section>
{/* ── 핵심 사업 영역 (Variant 스타일) ─────────────────── */}
<section className="section py-section bg-light">
{/* ── 핵심 사업 영역 ───────────────────────────────── */}
<section className="section section-business">
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '48px' }}>
<span className="section-label-v">저희 서비스</span>
<h2 className="section-title-v">핵심 전문성 솔루션</h2>
<div className="section-underline" />
<div className="section-header">
<span className="section-label">Our Business</span>
<h2 className="section-title">기업과 기관을 위한<br/><em>맞춤형 IT 솔루션</em></h2>
<div className="divider" />
</div>
<div className="grid-3">
<div className="business-grid">
{BUSINESS.map((b, i) => (
<Link to={b.path} key={i} style={{ textDecoration: 'none' }}>
<div className="v-card" style={{ height: '100%' }}>
<div className="v-icon-box">{b.svg}</div>
<h3 className="v-card-title">{b.title}</h3>
<p className="v-card-desc">{b.desc}</p>
<span style={{ display: 'inline-block', marginTop: '16px', fontSize: '14px',
fontWeight: 700, color: 'var(--v-cyan-500)', letterSpacing: '.02em' }}>
자세히 보기
</span>
<Link to={b.path} key={i} className="business-card">
<div className="business-icon" style={{ background: b.color + '18', color: b.color }}>
{b.icon}
</div>
<h3 className="business-title">{b.title}</h3>
<p className="business-desc">{b.desc}</p>
<span className="business-more" style={{ color: b.color }}>자세히 보기 </span>
</Link>
))}
</div>
</div>
</section>
{/* ── GUARDiA 솔루션 하이라이트 (Variant 스타일) ────────── */}
<section className="section section-guardia py-section">
{/* ── GUARDiA 솔루션 하이라이트 ──────────────────────── */}
<section className="section section-guardia">
<div className="container">
<div className="guardia-inner">
<div className="guardia-text">
<span className="section-label-v" style={{ textAlign: 'left' }}>대표 솔루션</span>
<h2 className="section-title-v" style={{ textAlign: 'left' }}>
GUARDiA ITSM<br/>
<span style={{ color: 'var(--v-cyan-500)' }}>AI 기반 인프라 자율 운영</span>
<span className="section-label">대표 솔루션</span>
<h2 className="section-title" style={{textAlign:'left'}}>
<em>GUARDiA ITSM</em><br/>
AI 기반 인프라 자율 운영
</h2>
<div className="section-underline left" />
<div className="divider divider-left" />
<p className="guardia-desc">
1,000 이상의 관공서 레거시 인프라를 메신저 명령으로 제어하는
온프레미스 AI ChatOps 플랫폼. 에이전트 설치 없이 SSH/SFTP만으로
@ -230,21 +194,19 @@ export default function Home() {
<div className="guardia-features">
{GUARDIA_FEATURES.map((f, i) => (
<div key={i} className="guardia-feature">
<span className="feature-icon" style={{ background: 'var(--v-blue-100)', borderRadius: '8px', padding: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '40px', height: '40px', flexShrink: 0 }}>
{f.svg}
</span>
<span className="feature-icon">{f.icon}</span>
<div>
<strong style={{ color: 'var(--v-navy-700)' }}>{f.label}</strong>
<p style={{ color: 'var(--v-gray-600)' }}>{f.desc}</p>
<strong>{f.label}</strong>
<p>{f.desc}</p>
</div>
</div>
))}
</div>
<div className="guardia-actions" style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<Link to="/solution/guardia" className="btn-v-primary">
<div className="guardia-actions">
<Link to="/solution/guardia" className="btn btn-primary">
GUARDiA 상세보기
</Link>
<Link to="/support/contact" className="btn-v-ghost">
<Link to="/support/contact" className="btn btn-outline">
도입 문의
</Link>
</div>
@ -280,37 +242,27 @@ export default function Home() {
</div>
</section>
{/* ── KPI 수치 (Variant 다크 네이비) ─────────────────── */}
<section className="v-stats-section">
{/* ── KPI 수치 ────────────────────────────────────────── */}
<section className="section-kpi">
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '56px' }}>
<span className="section-label-v" style={{ color: 'var(--v-cyan-400)' }}>숫자로 보는 지오정보기술</span>
<h2 style={{ fontSize: 'clamp(24px,3vw,36px)', fontWeight: 800, color: '#fff', marginTop: '8px' }}>
신뢰받는 IT 파트너
</h2>
</div>
<div className="grid-4">
<div className="kpi-grid">
{KPIS.map((k, i) => (
<div key={i} style={{ textAlign: 'center', padding: '24px 16px',
borderRight: i < KPIS.length - 1 ? '1px solid rgba(255,255,255,.1)' : 'none' }}>
<div className="v-stat-number">
{k.value.replace(/[+%년]/g, '')}
<span className="v-stat-unit">{k.value.match(/[+%년]/)?.[0] || ''}</span>
</div>
<div className="v-stat-label">{k.label}</div>
<div key={i} className="kpi-item">
<div className="kpi-value">{k.value}</div>
<div className="kpi-label">{k.label}</div>
</div>
))}
</div>
</div>
</section>
{/* ── 최신 소식 (Variant 스타일) ──────────────────────── */}
<section className="section section-news py-section bg-white">
{/* ── 최신 소식 ───────────────────────────────────────── */}
<section className="section section-news">
<div className="container">
<div className="section-header" style={{ marginBottom: '48px' }}>
<span className="section-label-v">최신 소식</span>
<h2 className="section-title-v">지오정보기술 소식</h2>
<div className="section-underline" />
<div className="section-header">
<span className="section-label">Latest News</span>
<h2 className="section-title">지오정보기술 <em>소식</em></h2>
<div className="divider" />
</div>
<div className="news-grid">
{news.length > 0 ? news.map(n => (
@ -346,28 +298,20 @@ export default function Home() {
</div>
</section>
{/* ── CTA 배너 (Variant v-cta-banner) ─────────────────── */}
<section className="py-section bg-light">
{/* ── CTA 배너 ────────────────────────────────────────── */}
<section className="section-cta">
<div className="container">
<div className="v-cta-banner">
<div>
<div className="v-cta-title">
GUARDiA ITSM 도입을 검토하고 계신가요?
</div>
<p className="v-cta-sub">
전문 컨설턴트가 기관 환경에 맞는 최적의 방안을 제안해 드립니다.
</p>
<div className="cta-inner">
<div className="cta-text">
<h2>GUARDiA ITSM 도입을 검토하고 계신가요?</h2>
<p>전문 컨설턴트가 기관 환경에 맞는 최적의 방안을 제안해 드립니다.</p>
</div>
<div style={{ display: 'flex', gap: '12px', flexShrink: 0, flexWrap: 'wrap' }}>
<Link to="/support/contact" className="btn-v-primary btn-v-cyan">
<div className="cta-actions">
<Link to="/support/contact" className="btn btn-white btn-lg">
무료 상담 신청
</Link>
<Link to="/solution/guardia"
style={{ display: 'inline-flex', alignItems: 'center', padding: '12px 24px',
background: 'rgba(255,255,255,.15)', color: '#fff', fontWeight: 600, fontSize: '15px',
borderRadius: '9999px', border: '1px solid rgba(255,255,255,.3)',
textDecoration: 'none', transition: 'all 240ms' }}>
제품 소개서
<Link to="/solution/guardia" className="btn btn-outline btn-lg" style={{color:'#fff',borderColor:'rgba(255,255,255,.5)'}}>
제품 소개서 다운로드
</Link>
</div>
</div>

View File

@ -1,203 +0,0 @@
import { useEffect, useState, useCallback } from 'react';
const token = () => localStorage.getItem('admin_token');
const authFetch = (url, opts = {}) =>
fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}`, ...opts.headers } });
const EMPTY = { year: '', content: '', sortOrder: 0, visible: true };
export default function AdminHistory() {
const [items, setItems] = useState([]);
const [modal, setModal] = useState(null); // null | 'create' | 'edit'
const [form, setForm] = useState(EMPTY);
const [editId, setEditId] = useState(null);
const [saving, setSaving] = useState(false);
const [toast, setToast] = useState(null);
const [search, setSearch] = useState('');
const showToast = (msg, type = 'success') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 2500);
};
const load = useCallback(() => {
authFetch('/api/admin/history').then(r => r.json()).then(setItems).catch(() => {});
}, []);
useEffect(() => { load(); }, [load]);
const openCreate = () => { setForm(EMPTY); setEditId(null); setModal('create'); };
const openEdit = h => { setForm({ year: h.year, content: h.content, sortOrder: h.sortOrder, visible: h.visible }); setEditId(h.id); setModal('edit'); };
const closeModal = () => { setModal(null); setForm(EMPTY); setEditId(null); };
const save = async () => {
if (!form.year.trim() || !form.content.trim()) { showToast('연도와 내용은 필수입니다.', 'error'); return; }
setSaving(true);
try {
const url = modal === 'edit' ? `/api/admin/history/${editId}` : '/api/admin/history';
const meth = modal === 'edit' ? 'PUT' : 'POST';
const r = await authFetch(url, { method: meth, body: JSON.stringify(form) });
if (!r.ok) throw new Error('저장 실패');
showToast(modal === 'edit' ? '수정되었습니다.' : '등록되었습니다.');
closeModal(); load();
} catch { showToast('저장 중 오류가 발생했습니다.', 'error'); }
finally { setSaving(false); }
};
const del = async id => {
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
await authFetch(`/api/admin/history/${id}`, { method: 'DELETE' });
showToast('삭제되었습니다.');
load();
};
const toggleVisible = async h => {
await authFetch(`/api/admin/history/${h.id}`, {
method: 'PUT',
body: JSON.stringify({ ...h, visible: !h.visible }),
});
load();
};
//
const grouped = {};
items.filter(h => !search || h.year.includes(search) || h.content.includes(search))
.forEach(h => { (grouped[h.year] = grouped[h.year] || []).push(h); });
return (
<main className="admin-page">
{toast && (
<div className={`admin-toast ${toast.type}`} style={{
position:'fixed', top:24, right:24, zIndex:9999,
padding:'12px 20px', borderRadius:8, color:'#fff',
background: toast.type === 'error' ? '#dc2626' : '#16a34a',
boxShadow:'0 4px 12px rgba(0,0,0,.15)', fontSize:14,
}}>{toast.msg}</div>
)}
<div style={{ display:'flex', alignItems:'center', gap:12, marginBottom:20 }}>
<h2 style={{ margin:0, fontSize:20, fontWeight:800 }}>연혁 관리</h2>
<input value={search} onChange={e=>setSearch(e.target.value)}
placeholder="연도 또는 내용 검색..."
style={{ padding:'6px 12px', border:'1px solid #cbd5e1', borderRadius:6, fontSize:13, flex:1, maxWidth:280 }} />
<button onClick={load} style={btnStyle('#64748b')}>새로고침</button>
<button onClick={openCreate} style={btnStyle('#4f6ef7')}>+ 항목 추가</button>
</div>
<div style={{ fontSize:13, color:'#64748b', marginBottom:16 }}>
<strong>{items.length}</strong> 항목
</div>
{/* 연도별 타임라인 테이블 */}
{Object.entries(grouped).map(([year, rows]) => (
<div key={year} style={{
background:'#fff', border:'1px solid #e2e8f0', borderRadius:10,
marginBottom:16, overflow:'hidden',
}}>
<div style={{
background:'#f1f5f9', padding:'10px 16px', fontWeight:700,
fontSize:16, color:'#1a3a6b', display:'flex', alignItems:'center', gap:8,
}}>
<span style={{ background:'#1a3a6b', color:'#fff', padding:'2px 10px', borderRadius:20, fontSize:13 }}>{year}</span>
<span style={{ fontSize:13, color:'#64748b', fontWeight:400 }}>{rows.length} 항목</span>
</div>
<table style={{ width:'100%', borderCollapse:'collapse', fontSize:13 }}>
<thead>
<tr style={{ background:'#f8fafc' }}>
{['순서','내용','노출','수정','삭제'].map(h=>(
<th key={h} style={{ padding:'8px 12px', textAlign:'left', color:'#475569', fontWeight:600, borderBottom:'1px solid #f1f5f9' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{rows.sort((a,b)=>a.sortOrder-b.sortOrder).map(h=>(
<tr key={h.id} style={{ borderTop:'1px solid #f8fafc' }}>
<td style={{ padding:'8px 12px', color:'#94a3b8', width:50 }}>{h.sortOrder}</td>
<td style={{ padding:'8px 12px', color: h.visible ? '#1e293b' : '#94a3b8' }}>
{!h.visible && <span style={{ fontSize:10, background:'#f1f5f9', color:'#94a3b8', borderRadius:4, padding:'1px 6px', marginRight:6 }}>숨김</span>}
{h.content}
</td>
<td style={{ padding:'8px 12px', width:60 }}>
<button onClick={()=>toggleVisible(h)} style={{
padding:'3px 10px', borderRadius:12, border:'none', cursor:'pointer', fontSize:11, fontWeight:600,
background: h.visible ? '#dcfce7' : '#f1f5f9',
color: h.visible ? '#16a34a' : '#94a3b8',
}}>{h.visible ? '공개' : '숨김'}</button>
</td>
<td style={{ padding:'8px 12px', width:60 }}>
<button onClick={()=>openEdit(h)} style={btnSmall('#4f6ef7')}>수정</button>
</td>
<td style={{ padding:'8px 12px', width:60 }}>
<button onClick={()=>del(h.id)} style={btnSmall('#dc2626')}>삭제</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
{Object.keys(grouped).length === 0 && (
<div style={{ textAlign:'center', padding:48, color:'#94a3b8' }}>
{search ? '검색 결과 없음' : '등록된 연혁이 없습니다.'}
</div>
)}
{/* 모달 */}
{modal && (
<div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,.4)', zIndex:1000, display:'flex', alignItems:'center', justifyContent:'center' }}>
<div style={{ background:'#fff', borderRadius:12, width:520, maxHeight:'90vh', overflowY:'auto', padding:28 }}>
<h3 style={{ margin:'0 0 20px', fontSize:18, fontWeight:800 }}>
{modal === 'create' ? '연혁 추가' : '연혁 수정'}
</h3>
<div style={{ display:'flex', flexDirection:'column', gap:14 }}>
<label style={labelStyle}>
연도 <span style={{ color:'#dc2626' }}>*</span>
<input value={form.year}
onChange={e=>setForm(p=>({...p, year:e.target.value}))}
placeholder="예: 2026 또는 20202021"
style={inputStyle} />
<small style={{ color:'#94a3b8' }}>범위 표시 예시: 20202021 (em dash 사용)</small>
</label>
<label style={labelStyle}>
내용 <span style={{ color:'#dc2626' }}>*</span>
<textarea value={form.content}
onChange={e=>setForm(p=>({...p, content:e.target.value}))}
placeholder="연혁 내용을 입력하세요"
rows={3} style={{ ...inputStyle, resize:'vertical' }} />
</label>
<label style={labelStyle}>
순서 (같은 연도 )
<input type="number" value={form.sortOrder} min={0}
onChange={e=>setForm(p=>({...p, sortOrder:+e.target.value}))}
style={{ ...inputStyle, width:120 }} />
</label>
<label style={{ display:'flex', alignItems:'center', gap:8, fontSize:13, cursor:'pointer' }}>
<input type="checkbox" checked={form.visible}
onChange={e=>setForm(p=>({...p, visible:e.target.checked}))} />
홈페이지에 공개
</label>
</div>
<div style={{ display:'flex', gap:10, marginTop:24, justifyContent:'flex-end' }}>
<button onClick={closeModal} style={btnStyle('#64748b')}>취소</button>
<button onClick={save} disabled={saving} style={btnStyle('#4f6ef7')}>
{saving ? '저장 중...' : '저장'}
</button>
</div>
</div>
</div>
)}
</main>
);
}
const btnStyle = color => ({
padding:'7px 16px', background:color, color:'#fff',
border:'none', borderRadius:6, cursor:'pointer', fontSize:13, fontWeight:600,
});
const btnSmall = color => ({
padding:'3px 10px', background:color, color:'#fff',
border:'none', borderRadius:4, cursor:'pointer', fontSize:11, fontWeight:600,
});
const labelStyle = { display:'flex', flexDirection:'column', gap:4, fontSize:13, fontWeight:600, color:'#475569' };
const inputStyle = { padding:'8px 10px', border:'1px solid #cbd5e1', borderRadius:6, fontSize:13, outline:'none', marginTop:2 };

View File

@ -7,7 +7,6 @@ const NAV = [
{ path: '/admin/dashboard', icon: '📊', label: '대시보드' },
{ section: '콘텐츠 관리' },
{ path: '/admin/news', icon: '📰', label: '뉴스/공지사항' },
{ path: '/admin/history', icon: '📅', label: '회사 연혁' },
{ path: '/admin/recruit', icon: '👥', label: '채용공고' },
{ section: '고객 관리' },
{ path: '/admin/inquiries', icon: '📩', label: '문의 관리', badgeKey: 'pendingInquiries' },

View File

@ -1,7 +1,6 @@
/* ============================================================
()지오정보기술 글로벌 스타일 Variant 디자인 적용
()지오정보기술 글로벌 스타일 URP 스타일 기반
============================================================ */
@import './tokens.css';
:root {
/* ── 브랜드 컬러 ── */
@ -28,8 +27,8 @@
--warning: #F59E0B;
--danger: #EF4444;
/* ── 타이포그래피 (Pretendard 우선) ── */
--font-sans: 'Pretendard', 'Noto Sans KR', 'Inter', -apple-system, sans-serif;
/* ── 타이포그래피 ── */
--font-sans: 'Noto Sans KR', 'Inter', -apple-system, sans-serif;
--font-en: 'Inter', sans-serif;
/* ── 레이아웃 ── */
@ -214,181 +213,3 @@ button { cursor: pointer; border: none; background: none; font-family: inherit;
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
.container { padding: 0 16px; }
}
/*
Variant 디자인 시스템 공통 UI 요소
*/
/* ── 섹션 라벨 (시안 소문자 + 굵은 텍스트) ── */
.section-label-v {
display: inline-block;
font-size: var(--v-label-size, 12px);
font-weight: var(--v-label-weight, 700);
letter-spacing: var(--v-label-spacing, 0.1em);
text-transform: uppercase;
color: var(--v-cyan-500, #00A0C8);
margin-bottom: 12px;
}
/* ── 섹션 제목 (Variant 스타일) ── */
.section-title-v {
font-size: clamp(28px, 3.5vw, 42px);
font-weight: 800;
color: var(--v-navy-700, #003366);
line-height: 1.2;
letter-spacing: -0.02em;
margin-bottom: 16px;
}
/* ── 언더라인 강조 (시안) ── */
.section-underline {
width: 48px; height: 3px;
background: var(--v-cyan-500, #00A0C8);
border-radius: 2px;
margin: 0 auto 40px;
}
.section-underline.left { margin-left: 0; }
/* ── 서비스 아이콘 카드 (Variant 스타일) ── */
.v-icon-box {
width: 64px; height: 64px;
background: var(--v-blue-100, #E8F0F8);
border-radius: var(--v-radius-md, 12px);
display: flex; align-items: center; justify-content: center;
margin-bottom: 20px;
transition: all var(--v-ease, 240ms);
}
.v-icon-box svg { width: 32px; height: 32px; }
.v-card:hover .v-icon-box {
background: var(--v-cyan-100, #E8F7FB);
transform: scale(1.08);
}
/* ── 서비스 카드 (Variant 스타일) ── */
.v-card {
background: #fff;
border-radius: var(--v-radius-lg, 16px);
border: 1px solid var(--v-gray-200, #E2E8F0);
padding: 32px 28px;
box-shadow: var(--v-shadow-sm);
transition: all var(--v-ease, 240ms);
}
.v-card:hover {
box-shadow: var(--v-shadow-lg);
transform: translateY(-4px);
border-color: var(--v-blue-200, #C5D8EF);
}
.v-card-title {
font-size: 18px; font-weight: 700;
color: var(--v-navy-700, #003366);
margin-bottom: 10px;
}
.v-card-desc {
font-size: 14px; line-height: 1.7;
color: var(--v-gray-600, #475569);
}
/* ── 통계 섹션 (다크 네이비) ── */
.v-stats-section {
background: var(--v-navy-800, #002752);
padding: var(--v-section-py, 96px) 0;
}
.v-stat-number {
font-size: clamp(32px, 4vw, 52px);
font-weight: 900;
color: #fff;
letter-spacing: -0.02em;
line-height: 1;
}
.v-stat-label {
font-size: 14px; color: rgba(255,255,255,.65);
margin-top: 8px; font-weight: 500;
}
.v-stat-unit {
font-size: 18px; font-weight: 700; color: var(--v-cyan-400, #29B8D8);
margin-left: 4px;
}
/* ── CTA 배너 (다크) ── */
.v-cta-banner {
background: linear-gradient(135deg, var(--v-navy-700), var(--v-navy-500));
border-radius: var(--v-radius-xl, 24px);
padding: 48px 56px;
display: flex; align-items: center; justify-content: space-between; gap: 32px;
box-shadow: var(--v-shadow-lg);
}
.v-cta-title {
font-size: clamp(20px, 2.5vw, 28px);
font-weight: 800; color: #fff;
}
.v-cta-sub {
font-size: 15px; color: rgba(255,255,255,.7);
margin-top: 8px;
}
/* ── Variant 버튼 스타일 ── */
.btn-v-primary {
display: inline-flex; align-items: center; gap: 8px;
padding: 13px 28px;
background: var(--v-navy-700, #003366);
color: #fff; font-weight: 700; font-size: 15px;
border-radius: var(--v-radius-full, 9999px);
border: none; cursor: pointer;
transition: all var(--v-ease, 240ms);
text-decoration: none;
}
.btn-v-primary:hover {
background: var(--v-navy-600, #004080);
box-shadow: var(--v-shadow-md);
transform: translateY(-2px);
}
.btn-v-ghost {
display: inline-flex; align-items: center; gap: 8px;
padding: 12px 28px;
background: transparent;
color: var(--v-navy-700, #003366); font-weight: 600; font-size: 15px;
border-radius: var(--v-radius-full, 9999px);
border: 2px solid var(--v-navy-700, #003366);
cursor: pointer; transition: all var(--v-ease, 240ms);
text-decoration: none;
}
.btn-v-ghost:hover {
background: var(--v-blue-100, #E8F0F8);
}
.btn-v-cyan {
background: var(--v-cyan-500, #00A0C8);
}
.btn-v-cyan:hover { background: var(--v-cyan-400, #29B8D8); }
/* ── 파트너 로고 바 ── */
.v-partners-bar {
display: flex; align-items: center; justify-content: center;
gap: 40px; flex-wrap: wrap;
padding: 32px 0;
border-top: 1px solid var(--v-gray-200);
border-bottom: 1px solid var(--v-gray-200);
margin: 32px 0;
}
.v-partner-item {
font-size: 15px; font-weight: 700;
color: var(--v-gray-400, #94A3B8);
display: flex; align-items: center; gap: 8px;
transition: color var(--v-ease);
}
.v-partner-item:hover { color: var(--v-navy-700); }
/* ── 섹션 배경 종류 ── */
.bg-white { background: #fff; }
.bg-light { background: var(--v-gray-50, #F8FAFC); }
.bg-navy { background: var(--v-navy-800, #002752); }
.bg-navy-dark { background: var(--v-navy-900, #001a33); }
/* ── 섹션 패딩 ── */
.py-section { padding: var(--v-section-py, 96px) 0; }
.py-section-sm { padding: var(--v-section-py-sm, 64px) 0; }
@media (max-width: 768px) {
.v-cta-banner { flex-direction: column; padding: 32px 24px; }
.v-stat-number { font-size: 28px; }
.v-section-py { padding: 60px 0; }
}

View File

@ -1,50 +0,0 @@
/* ── zio Variant 디자인 토큰 ── */
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
:root {
/* Variant 색상 팔레트 */
--v-navy-900: #001a33;
--v-navy-800: #002752;
--v-navy-700: #003366;
--v-navy-600: #004080;
--v-navy-500: #005A8C;
--v-cyan-500: #00A0C8;
--v-cyan-400: #29B8D8;
--v-cyan-100: #E8F7FB;
--v-blue-100: #E8F0F8;
--v-blue-200: #C5D8EF;
/* 뉴트럴 */
--v-gray-50: #F8FAFC;
--v-gray-100: #F1F5F9;
--v-gray-200: #E2E8F0;
--v-gray-400: #94A3B8;
--v-gray-600: #475569;
--v-gray-800: #1E293B;
--v-gray-900: #0F172A;
/* 섹션 라벨 */
--v-label-color: var(--v-cyan-500);
--v-label-size: 12px;
--v-label-weight: 700;
--v-label-spacing: 0.1em;
/* 간격 */
--v-section-py: 96px;
--v-section-py-sm: 64px;
/* 반경 */
--v-radius-sm: 8px;
--v-radius-md: 12px;
--v-radius-lg: 16px;
--v-radius-xl: 24px;
--v-radius-full: 9999px;
/* 그림자 */
--v-shadow-sm: 0 1px 3px rgba(0,51,102,.08), 0 1px 2px rgba(0,51,102,.06);
--v-shadow-md: 0 4px 12px rgba(0,51,102,.10), 0 2px 6px rgba(0,51,102,.06);
--v-shadow-lg: 0 10px 30px rgba(0,51,102,.12), 0 4px 10px rgba(0,51,102,.06);
/* 전환 */
--v-ease: 240ms cubic-bezier(.4,0,.2,1);
}