feat(zioinfo-web): (주)지오정보기술 홈페이지 Spring Boot+React 구현

Spring Boot 3.2 + React 18 SPA + SQLite
GUARDiA PMS ZIO-WEB-2026 프로젝트 연동
URP 스타일 디자인, GUARDiA 솔루션 상세 소개 (스크린샷 6장)
25개 ChatOps 봇 명령어 카탈로그, Messenger Bot 시나리오 데모
manual/17 개발가이드 생성

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-05-30 08:27:40 +09:00
parent a1b6f85917
commit e9f0455c94
42 changed files with 3132 additions and 0 deletions

View File

@ -0,0 +1,186 @@
# (주)지오정보기술 홈페이지 개발 가이드
> **프로젝트 코드:** ZIO-WEB-2026
> **기술 스택:** Spring Boot 3.2 + React 18 + SQLite
> **GUARDiA PMS:** http://localhost:8001/si (프로젝트 ID=1)
---
## 1. 프로젝트 구조
```
workspace/zioinfo-web/
├── backend/ # Spring Boot 백엔드 (포트 8080)
│ ├── pom.xml # Maven 의존성 (SQLite, JPA, Mail)
│ ├── src/main/
│ │ ├── java/kr/co/zioinfo/web/
│ │ │ ├── ZioinfoWebApplication.java
│ │ │ ├── controller/ApiController.java # 전체 REST API
│ │ │ ├── model/News.java
│ │ │ ├── model/Inquiry.java
│ │ │ ├── service/NewsService.java
│ │ │ ├── service/InquiryService.java
│ │ │ ├── repository/NewsRepository.java
│ │ │ ├── repository/InquiryRepository.java
│ │ │ └── config/DataInitializer.java # 초기 뉴스 데이터
│ │ └── resources/
│ │ └── application.yml # SQLite 설정
│ └── data/zioinfo.db # SQLite DB 파일 (자동 생성)
└── frontend/ # React SPA (포트 3000, 개발)
├── public/
│ ├── logo.png # 지오정보기술 로고
│ ├── logo-white.png # 흰색 로고 (푸터)
│ ├── favicon.ico
│ └── screenshots/ # GUARDiA 실행 화면 캡처 6장
├── src/
│ ├── components/layout/
│ │ ├── Header.jsx + .css # 고정 헤더 + 드롭다운 메뉴
│ │ └── Footer.jsx + .css # 회사 정보 + 링크
│ ├── pages/
│ │ ├── Home.jsx + .css # 메인 (히어로 슬라이더)
│ │ ├── GuardiaDetail.jsx + .css # GUARDiA 솔루션 상세
│ │ ├── Contact.jsx + .css # 문의하기 폼
│ │ ├── Company.jsx # 회사소개
│ │ ├── NewsPage.jsx # 뉴스룸
│ │ └── Recruit.jsx # 채용
│ ├── styles/global.css # 디자인 시스템 (변수/공통)
│ ├── App.jsx # 라우팅
│ └── main.jsx # 진입점
├── package.json
└── vite.config.js # Spring Boot 백엔드 프록시
```
---
## 2. 실행 방법
### 개발 환경 (핫 리로드)
```bash
# 터미널 1: Spring Boot 백엔드
cd workspace/zioinfo-web/backend
./mvnw spring-boot:run
# 터미널 2: React 프론트엔드
cd workspace/zioinfo-web/frontend
npm install
npm run dev # http://localhost:3000
```
### 운영 빌드 (단일 JAR)
```bash
# React 빌드 → Spring Boot static 폴더로 출력
cd frontend && npm run build
# Spring Boot 단일 JAR 빌드
cd ../backend && ./mvnw clean package -DskipTests
# 실행
java -jar target/zioinfo-web-1.0.0.jar
# → http://localhost:8080
```
---
## 3. API 엔드포인트
| Method | URL | 설명 |
|--------|-----|------|
| GET | `/api/company` | 회사 정보 |
| GET | `/api/history` | 회사 연혁 |
| GET | `/api/solutions/guardia` | GUARDiA 솔루션 정보 |
| GET | `/api/menu` | 메뉴 구조 |
| GET | `/api/news` | 소식 목록 (페이지) |
| GET | `/api/news/{id}` | 소식 상세 |
| POST | `/api/inquiry` | 문의 접수 |
---
## 4. DB 설정 (SQLite)
```yaml
# application.yml
spring:
datasource:
url: jdbc:sqlite:./data/zioinfo.db
driver-class-name: org.sqlite.JDBC
jpa:
database-platform: org.hibernate.community.dialect.SQLiteDialect
hibernate.ddl-auto: update
```
**DB 파일 위치:** `backend/data/zioinfo.db`
**MySQL 전환 시:** `application-prod.yml`에 MySQL 설정 후 `--spring.profiles.active=prod`
---
## 5. 메뉴 구성
| 1단계 | 2단계 서브메뉴 |
|-------|--------------|
| 회사소개 | CEO 인사말, 연혁, 조직도, CI 소개, 오시는 길 |
| 솔루션 | **GUARDiA ITSM** ★, ERP, CRM, BI |
| 사업실적 | 구축 레퍼런스, 파트너 |
| 고객지원 | 공지사항, FAQ, 카탈로그, 문의하기 |
| 채용 | 채용공고, 복리후생, 지원하기 |
| 뉴스 | 뉴스룸, 기술 블로그 |
---
## 6. GUARDiA 솔루션 소개 페이지 (`/solution/guardia`)
**탭 구성:**
1. **핵심 기능** — 실행 스크린샷 6장 + 8가지 기능 카드
2. **Messenger Bot** — 25개 명령어 카탈로그 + 운영 시나리오 데모
3. **에디션 비교** — COMMUNITY / STANDARD / ENTERPRISE
4. **기술 스택** — Backend/AI/Infra/Frontend/DevOps/모니터링
5. **도입 사례** — 광역기관/공공IT센터/교육청
---
## 7. 디자인 시스템
```css
/* 브랜드 컬러 */
--primary: #0051A2; /* 딥블루 */
--accent: #00A3E0; /* 포인트 */
--secondary: #1A1A2E; /* 다크 네이비 */
/* 폰트 */
--font-sans: 'Noto Sans KR', 'Inter', sans-serif;
```
**참조 디자인:** https://www.urpai.co.kr/
---
## 8. 스크린샷 추가/갱신
GUARDiA 화면이 변경될 때 재캡처:
```bash
cd workspace/zioinfo-web
python3 capture_screenshots.py # (추가 예정)
```
현재 캡처된 화면:
- `01_dashboard.png` — 통합 대시보드
- `02_sr_list.png` — SR 서비스 요청
- `03_si_project.png` — PMS 프로젝트
- `04_incidents.png` — 인시던트 관리
- `05_agents.png` — AI 에이전트
- `06_license.png` — 라이선스 관리
---
## 9. TODO (다음 단계)
- [ ] 관리자 페이지 (`/admin`) — 뉴스 CRUD, 문의 답변
- [ ] 채용 지원 폼 처리
- [ ] 오시는 길 지도 (카카오맵 연동)
- [ ] SEO 메타태그 완성
- [ ] 다크모드 대응
- [ ] E2E 테스트 (Playwright)
- [ ] Jenkins CI/CD 연동 (GUARDiA 배포 파이프라인)

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>kr.co.zioinfo</groupId>
<artifactId>zioinfo-web</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>ZioInfo Web</name>
<description>(주)지오정보기술 기업 홈페이지</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- SQLite (기본 DB — 파일 기반, 설치 없음) -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.3.0</version>
</dependency>
<!-- SQLite Hibernate Dialect (hibernate-community-dialects) -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
</dependency>
<!-- MySQL (운영 환경 전환 시 사용) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Mail (문의 이메일 발송) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package kr.co.zioinfo.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class ZioinfoWebApplication {
public static void main(String[] args) {
SpringApplication.run(ZioinfoWebApplication.class, args);
}
}

View File

@ -0,0 +1,58 @@
package kr.co.zioinfo.web.config;
import kr.co.zioinfo.web.model.News;
import kr.co.zioinfo.web.repository.NewsRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
private final NewsRepository newsRepo;
@Override
public void run(String... args) {
if (newsRepo.count() > 0) return;
newsRepo.save(News.builder()
.title("GUARDiA ITSM 2.0 정식 출시 — AI 기반 인프라 자율 운영 플랫폼")
.category("보도자료")
.summary("(주)지오정보기술이 공공기관 레거시 인프라 자동화를 위한 GUARDiA ITSM 2.0을 정식 출시했습니다.")
.content("GUARDiA ITSM 2.0은 메신저 한 줄 명령으로 에이전트리스 SSH/SFTP 배포·운영을 자동화하는 온프레미스 플랫폼입니다. " +
"1,000개 이상 관공서를 대상으로 하며 외부 클라우드 의존 없이 완전 폐쇄망 환경에서 동작합니다.")
.visible(true).viewCount(128).build());
newsRepo.save(News.builder()
.title("2026 공공기관 AI 인프라 혁신 박람회 참가")
.category("공지사항")
.summary("지오정보기술이 2026 공공기관 AI 인프라 혁신 박람회에 참가하여 GUARDiA 솔루션을 선보입니다.")
.content("박람회 기간: 2026년 6월 15일~17일 / 장소: 코엑스 A홀 / 부스: A-215\n" +
"GUARDiA ITSM 라이브 데모 및 도입 상담을 진행합니다.")
.visible(true).viewCount(87).build());
newsRepo.save(News.builder()
.title("행정안전부 공공SW 우수제품 선정")
.category("보도자료")
.summary("GUARDiA ITSM이 행정안전부 2026년 공공SW 우수제품으로 선정되었습니다.")
.content("행정안전부는 공공기관 정보화 사업에 적합한 소프트웨어를 심사하여 우수제품을 선정합니다. " +
"GUARDiA는 보안성, 안정성, 공공 적합성에서 높은 평가를 받았습니다.")
.visible(true).viewCount(214).build());
newsRepo.save(News.builder()
.title("Spring 2026 개발자 컨퍼런스 기술 발표")
.category("이벤트")
.summary("지오정보기술 개발팀이 AI 에이전트 기반 ChatOps 구현 사례를 발표합니다.")
.content("발표 주제: '1000개 관공서 인프라를 메신저 봇 하나로 관리하기'\n" +
"일시: 2026년 5월 20일 / 장소: 서울 COEX 컨퍼런스룸")
.visible(true).viewCount(63).build());
newsRepo.save(News.builder()
.title("특허 등록 — 에이전트리스 레거시 인프라 자동화 방법")
.category("공지사항")
.summary("에이전트 설치 없이 SSH/SFTP 프로토콜만으로 레거시 WAS를 자동화하는 방법에 대한 특허가 등록되었습니다.")
.content("특허명: 에이전트리스 레거시 인프라 자동화 시스템 및 방법\n등록번호: 10-2026-XXXXXXX")
.visible(true).viewCount(41).build());
}
}

View File

@ -0,0 +1,183 @@
package kr.co.zioinfo.web.controller;
import kr.co.zioinfo.web.model.Inquiry;
import kr.co.zioinfo.web.model.News;
import kr.co.zioinfo.web.service.InquiryService;
import kr.co.zioinfo.web.service.NewsService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.*;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@CrossOrigin(origins = {"http://localhost:3000", "http://localhost:5173"})
public class ApiController {
private final NewsService newsService;
private final InquiryService inquiryService;
// 회사 정보
@GetMapping("/company")
public ResponseEntity<Map<String, Object>> getCompanyInfo() {
Map<String, Object> info = new LinkedHashMap<>();
info.put("name", "(주)지오정보기술");
info.put("ceo", "대표이사");
info.put("founded", "2000년");
info.put("address", "서울특별시");
info.put("phone", "02-000-0000");
info.put("email", "info@zioinfo.co.kr");
info.put("business", Arrays.asList(
Map.of("name", "GUARDiA ITSM", "desc", "AI 기반 레거시 인프라 자율 운영 플랫폼"),
Map.of("name", "ERP 솔루션", "desc", "기업 자원관리 시스템"),
Map.of("name", "SI 구축", "desc", "정보화사업 시스템 통합"),
Map.of("name", "IT 인프라", "desc", "인프라 구축 및 운영")
));
return ResponseEntity.ok(info);
}
// 연혁
@GetMapping("/history")
public ResponseEntity<List<Map<String, Object>>> getHistory() {
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 솔루션 정보
@GetMapping("/solutions/guardia")
public ResponseEntity<Map<String, Object>> getGuardiaInfo() {
Map<String, Object> g = new LinkedHashMap<>();
g.put("name", "GUARDiA ITSM");
g.put("tagline", "AI 기반 레거시 인프라 자율 운영 플랫폼");
g.put("description",
"1,000개 이상 관공서 레거시 인프라를 대상으로 하는 AI 기반 통합 ChatOps 오케스트레이션 플랫폼. " +
"메신저 한 줄 명령으로 에이전트리스 배포·운영을 자동화합니다.");
g.put("keyFeatures", Arrays.asList(
Map.of("icon", "🤖", "title", "AI 에이전트 자동화",
"desc", "Ollama 온프레미스 sLLM 기반 자연어 명령 → 자동 배포·운영"),
Map.of("icon", "🔧", "title", "에이전트리스 아키텍처",
"desc", "대상 서버에 소프트웨어 설치 없이 SSH/SFTP만으로 관리"),
Map.of("icon", "💬", "title", "ChatOps 메신저 통합",
"desc", "카카오워크·네이버웍스·슬랙 등 메신저에서 직접 인프라 제어"),
Map.of("icon", "📊", "title", "통합 ITSM 대시보드",
"desc", "SR·인시던트·변경관리·SLA·CMDB 전체를 하나의 플랫폼에서"),
Map.of("icon", "🔒", "title", "엔터프라이즈 보안",
"desc", "AES-256-GCM 암호화, MFA, PAM, 불변 감사로그, Zero Trust"),
Map.of("icon", "🏗️", "title", "PMS (프로젝트 관리)",
"desc", "WBS·산출물·일주월 보고서 자동 생성, 이슈·위험 관리")
));
g.put("editions", Arrays.asList(
Map.of("name", "COMMUNITY", "price", "무료", "target", "소규모 기관",
"features", List.of("기본 SR 관리", "CMDB", "대시보드")),
Map.of("name", "STANDARD", "price", "협의", "target", "중형 기관",
"features", List.of("전체 ITSM", "AI 에이전트", "LDAP/MFA", "SLA")),
Map.of("name", "ENTERPRISE", "price", "협의", "target", "대형 관공서",
"features", List.of("무제한", "취약점 스캔", "Scouter APM", "FinOps", "전담 지원"))
));
g.put("techStack", Map.of(
"backend", "Python 3.11 / FastAPI",
"ai", "Ollama (온프레미스 sLLM, 외부 API 완전 금지)",
"infra", "paramiko SSH/SFTP (에이전트리스)",
"db", "PostgreSQL / SQLite",
"frontend", "React.js / PWA"
));
return ResponseEntity.ok(g);
}
// 소식 목록
@GetMapping("/news")
public ResponseEntity<Page<News>> getNews(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "6") int size,
@RequestParam(required = false) String category) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return ResponseEntity.ok(newsService.findAll(category, pageable));
}
@GetMapping("/news/{id}")
public ResponseEntity<News> getNewsDetail(@PathVariable Long id) {
return newsService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// 문의 접수
@PostMapping("/inquiry")
public ResponseEntity<Map<String, String>> submitInquiry(@Valid @RequestBody Inquiry inquiry) {
inquiryService.save(inquiry);
return ResponseEntity.ok(Map.of(
"message", "문의가 접수되었습니다. 빠른 시일 내에 연락드리겠습니다.",
"status", "SUCCESS"
));
}
// 메뉴 구조
@GetMapping("/menu")
public ResponseEntity<List<Map<String, Object>>> getMenu() {
return ResponseEntity.ok(List.of(
Map.of("id", "company", "label", "회사소개",
"children", List.of(
Map.of("id", "greeting", "label", "CEO 인사말", "path", "/company/greeting"),
Map.of("id", "history", "label", "연혁", "path", "/company/history"),
Map.of("id", "organization", "label", "조직도", "path", "/company/organization"),
Map.of("id", "ci", "label", "CI 소개", "path", "/company/ci"),
Map.of("id", "location", "label", "오시는 길", "path", "/company/location")
)),
Map.of("id", "solution", "label", "솔루션",
"children", List.of(
Map.of("id", "guardia", "label", "GUARDiA ITSM", "path", "/solution/guardia", "badge", "NEW"),
Map.of("id", "erp", "label", "ERP", "path", "/solution/erp"),
Map.of("id", "crm", "label", "CRM", "path", "/solution/crm"),
Map.of("id", "bi", "label", "BI", "path", "/solution/bi")
)),
Map.of("id", "business", "label", "사업실적",
"children", List.of(
Map.of("id", "reference", "label", "구축 레퍼런스", "path", "/business/reference"),
Map.of("id", "partner", "label", "파트너", "path", "/business/partner")
)),
Map.of("id", "support", "label", "고객지원",
"children", List.of(
Map.of("id", "notice", "label", "공지사항", "path", "/support/notice"),
Map.of("id", "faq", "label", "FAQ", "path", "/support/faq"),
Map.of("id", "catalog", "label", "카탈로그", "path", "/support/catalog"),
Map.of("id", "contact", "label", "문의하기", "path", "/support/contact")
)),
Map.of("id", "recruit", "label", "채용",
"children", List.of(
Map.of("id", "jobs", "label", "채용공고", "path", "/recruit/jobs"),
Map.of("id", "welfare", "label", "복리후생", "path", "/recruit/welfare"),
Map.of("id", "apply", "label", "지원하기", "path", "/recruit/apply")
)),
Map.of("id", "news", "label", "뉴스",
"children", List.of(
Map.of("id", "newsroom", "label", "뉴스룸", "path", "/news/newsroom"),
Map.of("id", "blog", "label", "기술 블로그", "path", "/news/blog")
))
));
}
}

View File

@ -0,0 +1,40 @@
package kr.co.zioinfo.web.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity @Table(name = "inquiry")
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Inquiry {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank @Column(nullable = false, length = 50)
private String name;
@NotBlank @Email @Column(nullable = false, length = 100)
private String email;
@Column(length = 30)
private String phone;
@NotBlank @Column(nullable = false, length = 200)
private String subject;
@NotBlank @Column(nullable = false, columnDefinition = "TEXT")
private String content;
@Column(length = 50)
private String category; // 제품문의 | 기술지원 | 사업제안 | 기타
private boolean agreePrivacy = false;
private String status = "PENDING"; // PENDING | ANSWERED
@CreatedDate
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,36 @@
package kr.co.zioinfo.web.model;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity @Table(name = "news")
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class News {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String title;
@Column(length = 50)
private String category; // 공지사항 | 보도자료 | 이벤트
@Column(columnDefinition = "TEXT")
private String content;
@Column(length = 500)
private String summary;
@Column(length = 300)
private String thumbnailUrl;
private boolean visible = true;
private int viewCount = 0;
@CreatedDate
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,4 @@
package kr.co.zioinfo.web.repository;
import kr.co.zioinfo.web.model.Inquiry;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InquiryRepository extends JpaRepository<Inquiry, Long> {}

View File

@ -0,0 +1,8 @@
package kr.co.zioinfo.web.repository;
import kr.co.zioinfo.web.model.News;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.repository.JpaRepository;
public interface NewsRepository extends JpaRepository<News, Long> {
Page<News> findByVisibleTrue(Pageable p);
Page<News> findByCategoryAndVisibleTrue(String category, Pageable p);
}

View File

@ -0,0 +1,21 @@
package kr.co.zioinfo.web.service;
import kr.co.zioinfo.web.model.Inquiry;
import kr.co.zioinfo.web.repository.InquiryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class InquiryService {
private final InquiryRepository repo;
public Inquiry save(Inquiry inquiry) {
Inquiry saved = repo.save(inquiry);
log.info("문의 접수: id={} name={} subject={}", saved.getId(), saved.getName(), saved.getSubject());
// TODO: 이메일 발송 (MailService 연동)
return saved;
}
}

View File

@ -0,0 +1,28 @@
package kr.co.zioinfo.web.service;
import kr.co.zioinfo.web.model.News;
import kr.co.zioinfo.web.repository.NewsRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class NewsService {
private final NewsRepository repo;
public Page<News> findAll(String category, Pageable pageable) {
if (category != null && !category.isBlank()) {
return repo.findByCategoryAndVisibleTrue(category, pageable);
}
return repo.findByVisibleTrue(pageable);
}
public Optional<News> findById(Long id) {
return repo.findById(id).map(n -> {
n.setViewCount(n.getViewCount() + 1);
return repo.save(n);
});
}
}

View File

@ -0,0 +1,49 @@
server:
port: 8080
servlet:
encoding:
charset: UTF-8
force: true
spring:
application:
name: zioinfo-web
datasource:
# SQLite — 파일 기반 DB (data/ 디렉토리 자동 생성)
url: jdbc:sqlite:./data/zioinfo.db
driver-class-name: org.sqlite.JDBC
jpa:
database-platform: org.hibernate.community.dialect.SQLiteDialect
hibernate:
ddl-auto: update # 스키마 자동 갱신 (운영: validate)
show-sql: false
properties:
hibernate:
format_sql: true
# SQLite는 foreign key 비활성 기본 → 명시 활성화
javax.persistence.schema-generation.database.action: none
mail:
host: ${MAIL_HOST:smtp.gmail.com}
port: ${MAIL_PORT:587}
username: ${MAIL_USERNAME:}
password: ${MAIL_PASSWORD:}
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
zioinfo:
company:
name: (주)지오정보기술
email: info@zioinfo.co.kr
phone: 02-000-0000
address: 서울특별시
cors:
allowed-origins:
- http://localhost:3000
- http://localhost:5173
- http://www.zioinfo.co.kr
logging:
level:
kr.co.zioinfo: DEBUG
org.hibernate.SQL: WARN

View File

@ -0,0 +1,26 @@
{
"name": "zioinfo-web-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"axios": "^1.7.2",
"swiper": "^11.1.4",
"react-intersection-observer": "^9.10.3",
"react-countup": "^6.5.3",
"react-scroll": "^1.9.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"vite": "^5.2.11"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="(주)지오정보기술 — AI 기반 레거시 인프라 자율 운영 플랫폼 GUARDiA ITSM 및 ERP·CRM·BI 솔루션">
<meta name="keywords" content="지오정보기술, GUARDiA, ITSM, 인프라자동화, 공공기관, ERP, ChatOps">
<title>(주)지오정보기술</title>
<link rel="icon" type="image/png" href="/favicon.png">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -0,0 +1,42 @@
import React, { Suspense, lazy } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import Header from './components/layout/Header';
import Footer from './components/layout/Footer';
const Home = lazy(() => import('./pages/Home'));
const GuardiaDetail = lazy(() => import('./pages/GuardiaDetail'));
const Company = lazy(() => import('./pages/Company'));
const Contact = lazy(() => import('./pages/Contact'));
const NewsPage = lazy(() => import('./pages/NewsPage'));
const Recruit = lazy(() => import('./pages/Recruit'));
const NotFound = lazy(() => import('./pages/NotFound'));
function Loading() {
return (
<div style={{display:'flex',alignItems:'center',justifyContent:'center',
height:'60vh',color:'var(--gray-400)',fontSize:'14px'}}>
로딩 ...
</div>
);
}
export default function App() {
const location = useLocation();
return (
<>
<Header />
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/solution/guardia" element={<GuardiaDetail />} />
<Route path="/company/*" element={<Company />} />
<Route path="/support/contact" element={<Contact />} />
<Route path="/news/*" element={<NewsPage />} />
<Route path="/recruit/*" element={<Recruit />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
<Footer />
</>
);
}

View File

@ -0,0 +1,69 @@
/* ─── Footer ──────────────────────────────────────────────── */
.footer { background: var(--secondary); color: rgba(255,255,255,.8); }
.footer-top { padding: 60px 0; }
.footer-top-inner {
display: grid;
grid-template-columns: 280px repeat(4, 1fr);
gap: 40px;
}
.footer-brand { }
.footer-logo { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.footer-logo img { height: 36px; }
.footer-logo-text { font-size: 20px; font-weight: 700; color: #fff; }
.footer-logo-text strong { color: var(--accent); }
.footer-tagline {
font-size: 13px; line-height: 1.8;
color: rgba(255,255,255,.6);
margin-bottom: 20px;
}
.footer-contact-list { display: flex; flex-direction: column; gap: 8px; }
.footer-contact-item { display: flex; gap: 10px; font-size: 13px; }
.contact-label { color: rgba(255,255,255,.4); min-width: 60px; }
.footer-contact-item a { color: var(--accent); }
.footer-contact-item a:hover { text-decoration: underline; }
.footer-menu-group { }
.footer-menu-title {
font-size: 13px; font-weight: 700;
color: #fff; letter-spacing: .5px;
text-transform: uppercase;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255,255,255,.1);
}
.footer-menu-list { display: flex; flex-direction: column; gap: 10px; }
.footer-menu-list a {
font-size: 13px; color: rgba(255,255,255,.6);
transition: color var(--fast);
}
.footer-menu-list a:hover { color: var(--accent); }
/* 하단 바 */
.footer-bottom {
border-top: 1px solid rgba(255,255,255,.08);
padding: 18px 0;
}
.footer-bottom-inner {
display: flex; align-items: center; gap: 24px;
font-size: 12px; color: rgba(255,255,255,.4);
}
.footer-legal { display: flex; gap: 16px; }
.footer-legal a { color: rgba(255,255,255,.4); }
.footer-legal a:hover { color: rgba(255,255,255,.8); }
.footer-copyright { flex: 1; text-align: center; }
.footer-powered { color: rgba(255,255,255,.3); }
.footer-powered strong { color: var(--accent); }
@media (max-width: 1024px) {
.footer-top-inner {
grid-template-columns: 1fr 1fr;
}
.footer-brand { grid-column: 1 / -1; }
}
@media (max-width: 768px) {
.footer-top-inner { grid-template-columns: 1fr 1fr; }
.footer-bottom-inner { flex-direction: column; text-align: center; gap: 12px; }
.footer-copyright { order: -1; }
}

View File

@ -0,0 +1,111 @@
import React from 'react';
import { Link } from 'react-router-dom';
import './Footer.css';
const FOOTER_MENUS = [
{
title: '회사소개',
links: [
{ label: 'CEO 인사말', path: '/company/greeting' },
{ label: '연혁', path: '/company/history' },
{ label: '조직도', path: '/company/organization' },
{ label: '오시는 길', path: '/company/location' },
]
},
{
title: '솔루션',
links: [
{ label: 'GUARDiA ITSM', path: '/solution/guardia' },
{ label: 'ERP', path: '/solution/erp' },
{ label: 'CRM', path: '/solution/crm' },
{ label: 'BI', path: '/solution/bi' },
]
},
{
title: '고객지원',
links: [
{ label: '공지사항', path: '/support/notice' },
{ label: 'FAQ', path: '/support/faq' },
{ label: '카탈로그', path: '/support/catalog' },
{ label: '문의하기', path: '/support/contact' },
]
},
{
title: '채용',
links: [
{ label: '채용공고', path: '/recruit/jobs' },
{ label: '복리후생', path: '/recruit/welfare' },
{ label: '지원하기', path: '/recruit/apply' },
]
},
];
export default function Footer() {
return (
<footer className="footer" role="contentinfo">
<div className="footer-top">
<div className="container footer-top-inner">
{/* 회사 정보 */}
<div className="footer-brand">
<Link to="/" className="footer-logo">
<img src="/logo-white.png" alt="(주)지오정보기술 로고" height="36"
onError={e => { e.target.style.display='none'; e.target.nextSibling.style.display='block'; }} />
<span className="footer-logo-text" style={{display:'none'}}>
<strong>Zio</strong>Info
</span>
</Link>
<p className="footer-tagline">
AI 기반 레거시 인프라 자율 운영 플랫폼<br/>
GUARDiA ITSM으로 공공기관 IT를 혁신합니다.
</p>
<div className="footer-contact-list">
<div className="footer-contact-item">
<span className="contact-label">대표전화</span>
<span>02-000-0000</span>
</div>
<div className="footer-contact-item">
<span className="contact-label">이메일</span>
<a href="mailto:info@zioinfo.co.kr">info@zioinfo.co.kr</a>
</div>
<div className="footer-contact-item">
<span className="contact-label">주소</span>
<span>서울특별시</span>
</div>
</div>
</div>
{/* 메뉴 그룹 */}
{FOOTER_MENUS.map((group, i) => (
<div key={i} className="footer-menu-group">
<h3 className="footer-menu-title">{group.title}</h3>
<ul className="footer-menu-list">
{group.links.map((link, j) => (
<li key={j}>
<Link to={link.path}>{link.label}</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
{/* 하단 바 */}
<div className="footer-bottom">
<div className="container footer-bottom-inner">
<div className="footer-legal">
<Link to="/privacy">개인정보처리방침</Link>
<Link to="/terms">이용약관</Link>
<Link to="/sitemap">사이트맵</Link>
</div>
<p className="footer-copyright">
Copyright &copy; 2026 ()지오정보기술 All Rights Reserved.
</p>
<div className="footer-powered">
Powered by <strong>GUARDiA ITSM</strong>
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,126 @@
/* ─── Header ──────────────────────────────────────────────── */
.skip-link {
position: absolute; top: -60px; left: 0; z-index: 9999;
background: var(--primary); color: #fff;
padding: 10px 20px; border-radius: 0 0 8px 0;
transition: top .2s;
}
.skip-link:focus { top: 0; }
.header {
position: fixed; top: 0; left: 0; right: 0;
z-index: 1000; height: var(--header-h);
background: rgba(26, 26, 46, 0.96);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255,255,255,.08);
transition: all var(--mid) var(--ease);
}
.header.scrolled {
background: rgba(26, 26, 46, 0.99);
box-shadow: 0 4px 24px rgba(0,0,0,.3);
}
.header-inner {
display: flex; align-items: center; gap: 32px;
height: 100%;
}
/* 로고 */
.logo { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.logo img { height: 40px; width: auto; filter: brightness(0) invert(1); }
.logo-text { color: #fff; font-size: 20px; font-weight: 700; }
.logo-text strong { color: var(--accent); }
/* 데스크톱 메뉴 */
.nav-desktop {
display: flex; align-items: center; gap: 4px;
margin-left: 24px; flex: 1;
}
.nav-item { position: relative; }
.nav-trigger {
height: var(--header-h);
padding: 0 16px;
color: rgba(255,255,255,.85);
font-size: 15px; font-weight: 500;
transition: color var(--fast);
display: flex; align-items: center;
}
.nav-trigger:hover,
.nav-item.active .nav-trigger { color: #fff; }
.nav-item.active .nav-trigger { border-bottom: 2px solid var(--accent); }
/* 드롭다운 */
.dropdown {
position: absolute; top: calc(var(--header-h) - 2px); left: 0;
min-width: 180px;
background: #fff;
border-radius: 0 0 var(--radius) var(--radius);
box-shadow: var(--shadow-lg);
border-top: 3px solid var(--primary);
padding: 8px 0;
animation: fadeDown .18s ease;
}
@keyframes fadeDown {
from { opacity:0; transform:translateY(-8px); }
to { opacity:1; transform:translateY(0); }
}
.dropdown-item {
display: flex; align-items: center; gap: 8px;
padding: 10px 20px;
font-size: 14px; color: var(--gray-700);
transition: all var(--fast);
}
.dropdown-item:hover, .dropdown-item.current {
background: var(--primary-light);
color: var(--primary);
}
/* CTA 버튼 */
.header-cta { margin-left: auto; flex-shrink: 0; }
/* 햄버거 */
.hamburger {
display: none;
flex-direction: column;
gap: 5px;
padding: 8px;
margin-left: auto;
}
.hamburger span {
display: block; width: 24px; height: 2px;
background: #fff;
border-radius: 2px;
transition: all var(--mid);
}
/* 모바일 메뉴 */
.nav-mobile {
display: none;
flex-direction: column;
background: var(--secondary);
border-top: 1px solid rgba(255,255,255,.1);
max-height: calc(100vh - var(--header-h));
overflow-y: auto;
}
.mobile-group { border-bottom: 1px solid rgba(255,255,255,.08); }
.mobile-group-header {
display: flex; align-items: center;
padding: 14px 24px;
color: rgba(255,255,255,.85);
font-size: 15px; font-weight: 500;
cursor: pointer;
}
.mobile-children { background: rgba(0,0,0,.2); }
.mobile-child {
display: flex; align-items: center; gap: 8px;
padding: 10px 36px;
font-size: 14px; color: rgba(255,255,255,.7);
}
.mobile-child:hover { color: #fff; }
/* 반응형 */
@media (max-width: 1024px) {
.nav-desktop, .header-cta { display: none; }
.hamburger { display: flex; }
.header.mobile-open .nav-mobile { display: flex; }
.header.mobile-open { height: auto; }
}

View File

@ -0,0 +1,159 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Link, useLocation } from 'react-router-dom';
import './Header.css';
const MENU = [
{
id: 'company', label: '회사소개',
children: [
{ label: 'CEO 인사말', path: '/company/greeting' },
{ label: '연혁', path: '/company/history' },
{ label: '조직도', path: '/company/organization' },
{ label: 'CI 소개', path: '/company/ci' },
{ label: '오시는 길', path: '/company/location' },
]
},
{
id: 'solution', label: '솔루션',
children: [
{ label: 'GUARDiA ITSM', path: '/solution/guardia', badge: 'NEW' },
{ label: 'ERP', path: '/solution/erp' },
{ label: 'CRM', path: '/solution/crm' },
{ label: 'BI', path: '/solution/bi' },
]
},
{
id: 'business', label: '사업실적',
children: [
{ label: '구축 레퍼런스', path: '/business/reference' },
{ label: '파트너', path: '/business/partner' },
]
},
{
id: 'support', label: '고객지원',
children: [
{ label: '공지사항', path: '/support/notice' },
{ label: 'FAQ', path: '/support/faq' },
{ label: '카탈로그', path: '/support/catalog' },
{ label: '문의하기', path: '/support/contact' },
]
},
{
id: 'recruit', label: '채용',
children: [
{ label: '채용공고', path: '/recruit/jobs' },
{ label: '복리후생', path: '/recruit/welfare' },
{ label: '지원하기', path: '/recruit/apply' },
]
},
{
id: 'news', label: '뉴스',
children: [
{ label: '뉴스룸', path: '/news/newsroom' },
{ label: '기술 블로그', path: '/news/blog' },
]
},
];
export default function Header() {
const [scrolled, setScrolled] = useState(false);
const [activeMenu, setActiveMenu] = useState(null);
const [mobileOpen, setMobileOpen] = useState(false);
const location = useLocation();
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 60);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
setMobileOpen(false);
setActiveMenu(null);
}, [location]);
const isActive = (menu) =>
menu.children?.some(c => location.pathname.startsWith(c.path));
return (
<>
{/* 접근성 스킵 링크 */}
<a href="#main-content" className="skip-link">본문 바로가기</a>
<header className={`header ${scrolled ? 'scrolled' : ''} ${mobileOpen ? 'mobile-open' : ''}`}
role="banner">
<div className="header-inner container">
{/* 로고 */}
<Link to="/" className="logo" aria-label="(주)지오정보기술 홈으로">
<img src="/logo.png" alt="(주)지오정보기술 로고" height="40"
onError={e => { e.target.style.display='none'; e.target.nextSibling.style.display='flex'; }} />
<span className="logo-text" style={{display:'none'}}>
<strong>Zio</strong>Info
</span>
</Link>
{/* 데스크톱 메뉴 */}
<nav className="nav-desktop" role="navigation" aria-label="주요 메뉴">
{MENU.map(menu => (
<div key={menu.id}
className={`nav-item ${isActive(menu) ? 'active' : ''}`}
onMouseEnter={() => setActiveMenu(menu.id)}
onMouseLeave={() => setActiveMenu(null)}>
<button className="nav-trigger" aria-haspopup="true"
aria-expanded={activeMenu === menu.id}>
{menu.label}
</button>
{activeMenu === menu.id && (
<div className="dropdown" role="menu">
{menu.children.map(child => (
<Link key={child.path} to={child.path}
className={`dropdown-item ${location.pathname === child.path ? 'current' : ''}`}
role="menuitem">
{child.label}
{child.badge && <span className="badge badge-new">{child.badge}</span>}
</Link>
))}
</div>
)}
</div>
))}
</nav>
{/* 문의 버튼 */}
<Link to="/support/contact" className="btn btn-primary btn-sm header-cta">
문의하기
</Link>
{/* 햄버거 (모바일) */}
<button className="hamburger" aria-label="모바일 메뉴"
aria-expanded={mobileOpen}
onClick={() => setMobileOpen(v => !v)}>
<span/><span/><span/>
</button>
</div>
{/* 모바일 메뉴 */}
{mobileOpen && (
<nav className="nav-mobile" role="navigation" aria-label="모바일 메뉴">
{MENU.map(menu => (
<details key={menu.id} className="mobile-group">
<summary className="mobile-group-header">{menu.label}</summary>
<div className="mobile-children">
{menu.children.map(child => (
<Link key={child.path} to={child.path} className="mobile-child">
{child.label}
{child.badge && <span className="badge badge-new">{child.badge}</span>}
</Link>
))}
</div>
</details>
))}
<Link to="/support/contact" className="btn btn-primary" style={{margin:'16px'}}>
문의하기
</Link>
</nav>
)}
</header>
</>
);
}

View File

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles/global.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

View File

@ -0,0 +1,7 @@
.inner-page { padding-top: var(--header-h); }
.page-hero {
background: linear-gradient(135deg, var(--secondary), var(--primary-dark));
padding: 60px 0; color: #fff;
}
.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; }

View File

@ -0,0 +1,38 @@
import React from "react";
import "./Common.css";
const SECTIONS = {
greeting: {
title: "CEO 인사말",
content: `안녕하세요. (주)지오정보기술 대표이사입니다.
저희 지오정보기술은 2000 창립 이래 20 년간 공공기관 IT 전문 기업으로 성장해 왔습니다.
최근 GUARDiA ITSM 플랫폼을 통해 AI 기반 인프라 자율 운영이라는 새로운 패러다임을 제시하고 있습니다. 메신저 명령으로 1,000 이상의 관공서 레거시 인프라를 자동화하는 혁신적인 솔루션으로, 공공기관의 디지털 전환을 이끌고 있습니다.
앞으로도 고객의 성공이 저희의 성공이라는 신념 아래, 최고의 기술력과 서비스로 보답하겠습니다.
감사합니다.`
},
};
export default function Company() {
return (
<main id="main-content" className="inner-page">
<div className="page-hero">
<div className="container">
<span className="section-label">Company</span>
<h1 className="page-hero-title">회사소개</h1>
</div>
</div>
<section className="section">
<div className="container" style={{maxWidth:"800px"}}>
<h2 style={{fontSize:"28px",fontWeight:"800",marginBottom:"24px"}}>CEO 인사말</h2>
{SECTIONS.greeting.content.split("\n\n").map((p,i) => (
<p key={i} style={{fontSize:"16px",lineHeight:"1.9",color:"var(--gray-700)",marginBottom:"20px"}}>{p}</p>
))}
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,42 @@
.contact-page { padding-top: var(--header-h); }
.page-hero {
background: linear-gradient(135deg, var(--secondary), var(--primary-dark));
padding: 60px 0;
color: #fff;
}
.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; }
.contact-grid { display: grid; grid-template-columns: 340px 1fr; gap: 48px; align-items: start; }
.contact-info h2 { font-size: 22px; font-weight: 700; margin-bottom: 28px; }
.info-item { display: flex; gap: 16px; margin-bottom: 24px; align-items: flex-start; }
.info-icon { font-size: 24px; }
.info-item strong { display: block; font-size: 13px; font-weight: 700; color: var(--gray-500); margin-bottom: 2px; }
.info-item p { font-size: 15px; color: var(--gray-800); }
.contact-form { padding: 36px; }
.contact-form h2 { font-size: 22px; font-weight: 700; margin-bottom: 24px; }
.form-alert { padding: 12px 16px; border-radius: var(--radius-sm); font-size: 14px; margin-bottom: 16px; }
.form-alert.success { background: #d1fae5; color: #065f46; }
.form-alert.error { background: #fee2e2; color: #991b1b; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.form-group label { font-size: 13px; font-weight: 600; color: var(--gray-700); }
.required { color: var(--danger); }
.form-group input, .form-group select, .form-group textarea {
padding: 10px 14px;
border: 1px solid var(--gray-200);
border-radius: var(--radius-sm);
font-size: 14px; font-family: inherit;
transition: border-color var(--fast);
outline: none;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0,81,162,.1);
}
.privacy-agree { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--gray-600); margin-bottom: 20px; cursor: pointer; }
.privacy-agree a { color: var(--primary); }
@media (max-width: 1024px) { .contact-grid { grid-template-columns: 1fr; } }
@media (max-width: 768px) { .form-row { grid-template-columns: 1fr; } }

View File

@ -0,0 +1,126 @@
import React, { useState } from 'react';
import axios from 'axios';
import './Contact.css';
export default function Contact() {
const [form, setForm] = useState({
name:'', email:'', phone:'', category:'제품문의', subject:'', content:'', agreePrivacy: false
});
const [status, setStatus] = useState(null);
const [loading, setLoading] = useState(false);
const handleChange = e => {
const { name, value, type, checked } = e.target;
setForm(f => ({ ...f, [name]: type === 'checkbox' ? checked : value }));
};
const handleSubmit = async e => {
e.preventDefault();
if (!form.agreePrivacy) { setStatus({ type: 'error', msg: '개인정보 수집·이용에 동의해주세요.' }); return; }
setLoading(true);
try {
await axios.post('/api/inquiry', form);
setStatus({ type: 'success', msg: '문의가 접수되었습니다. 빠른 시일 내에 연락드리겠습니다.' });
setForm({ name:'', email:'', phone:'', category:'제품문의', subject:'', content:'', agreePrivacy: false });
} catch {
setStatus({ type: 'error', msg: '문의 접수 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' });
} finally {
setLoading(false);
}
};
return (
<main id="main-content" className="contact-page">
<div className="page-hero">
<div className="container">
<span className="section-label">Contact Us</span>
<h1 className="page-hero-title">문의하기</h1>
<p>GUARDiA ITSM 도입 문의 제품 상담을 받아드립니다.</p>
</div>
</div>
<section className="section">
<div className="container contact-grid">
{/* 연락처 정보 */}
<div className="contact-info">
<h2>연락처 정보</h2>
{[
{ icon: '📞', label: '대표전화', value: '02-000-0000' },
{ icon: '✉️', label: '이메일', value: 'info@zioinfo.co.kr' },
{ icon: '🕐', label: '운영시간', value: '평일 09:00 ~ 18:00' },
{ icon: '📍', label: '주소', value: '서울특별시' },
].map((c,i) => (
<div key={i} className="info-item">
<span className="info-icon">{c.icon}</span>
<div>
<strong>{c.label}</strong>
<p>{c.value}</p>
</div>
</div>
))}
</div>
{/* 문의 폼 */}
<form className="contact-form card" onSubmit={handleSubmit}>
<h2>온라인 문의</h2>
{status && (
<div className={`form-alert ${status.type}`}>
{status.type === 'success' ? '✅' : '❌'} {status.msg}
</div>
)}
<div className="form-row">
<div className="form-group">
<label htmlFor="name">성함 <span className="required">*</span></label>
<input id="name" name="name" type="text" required
value={form.name} onChange={handleChange} placeholder="홍길동" />
</div>
<div className="form-group">
<label htmlFor="phone">연락처</label>
<input id="phone" name="phone" type="tel"
value={form.phone} onChange={handleChange} placeholder="010-0000-0000" />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="email">이메일 <span className="required">*</span></label>
<input id="email" name="email" type="email" required
value={form.email} onChange={handleChange} placeholder="your@email.com" />
</div>
<div className="form-group">
<label htmlFor="category">문의 유형</label>
<select id="category" name="category" value={form.category} onChange={handleChange}>
<option>제품문의</option>
<option>데모 신청</option>
<option>기술지원</option>
<option>사업제안</option>
<option>채용문의</option>
<option>기타</option>
</select>
</div>
</div>
<div className="form-group">
<label htmlFor="subject">제목 <span className="required">*</span></label>
<input id="subject" name="subject" type="text" required
value={form.subject} onChange={handleChange} placeholder="문의 제목을 입력해주세요" />
</div>
<div className="form-group">
<label htmlFor="content">문의 내용 <span className="required">*</span></label>
<textarea id="content" name="content" rows={6} required
value={form.content} onChange={handleChange}
placeholder="문의 내용을 자세히 작성해주세요." />
</div>
<label className="privacy-agree">
<input type="checkbox" name="agreePrivacy"
checked={form.agreePrivacy} onChange={handleChange} />
<span>개인정보 수집·이용에 동의합니다. <a href="/privacy" target="_blank">[보기]</a></span>
</label>
<button type="submit" className="btn btn-primary btn-lg" style={{width:'100%'}} disabled={loading}>
{loading ? '전송 중...' : '문의 접수하기'}
</button>
</form>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,281 @@
/* ─── GUARDiA 상세 페이지 ────────────────────────────────── */
.guardia-page { padding-top: var(--header-h); }
/* ── 히어로 ── */
.gd-hero {
position: relative;
background: linear-gradient(135deg, #0a0f24 0%, #001f5c 50%, #0051A2 100%);
padding: 80px 0 60px;
overflow: hidden;
}
.gd-hero::before {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse at 70% 50%, rgba(0,163,224,.15) 0%, transparent 70%);
}
.gd-hero-overlay { position: absolute; inset: 0; background: rgba(0,0,0,.2); }
.gd-hero-inner {
position: relative; z-index: 2;
display: grid; grid-template-columns: 1fr auto;
gap: 60px; align-items: center;
}
.gd-hero-title {
font-size: clamp(40px, 5vw, 64px);
font-weight: 900; color: #fff;
margin: 12px 0 16px;
}
.gd-hero-title span { color: var(--accent); }
.gd-hero-sub {
font-size: 18px; color: rgba(255,255,255,.8);
line-height: 1.7; margin-bottom: 32px;
}
.gd-hero-sub strong { color: #fff; }
.gd-hero-actions { display: flex; gap: 16px; flex-wrap: wrap; }
.gd-hero-stats {
display: grid; grid-template-columns: repeat(2,1fr);
gap: 16px; flex-shrink: 0;
}
.gd-stat {
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.15);
border-radius: var(--radius);
padding: 20px 24px; text-align: center;
}
.gd-stat-val { font-size: 28px; font-weight: 900; color: var(--accent); }
.gd-stat-lab { font-size: 12px; color: rgba(255,255,255,.7); margin-top: 4px; }
/* ── 탭 바 ── */
.gd-tabs-bar {
background: var(--white);
border-bottom: 2px solid var(--gray-200);
position: sticky; top: var(--header-h); z-index: 100;
}
.gd-tabs { display: flex; gap: 0; overflow-x: auto; }
.gd-tab {
padding: 16px 24px;
font-size: 14px; font-weight: 600;
color: var(--gray-600);
border-bottom: 3px solid transparent;
margin-bottom: -2px;
white-space: nowrap;
transition: all var(--fast);
}
.gd-tab:hover { color: var(--primary); }
.gd-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
/* ── 핵심 기능 ── */
.gd-features-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.gd-feature-card {
padding: 28px;
display: flex; flex-direction: column; gap: 12px;
}
.gd-feature-icon { font-size: 36px; }
.gd-feature-card h3 { font-size: 16px; font-weight: 700; color: var(--gray-900); }
.gd-feature-card p { font-size: 14px; color: var(--gray-600); line-height: 1.7; }
/* ── 스크린샷 갤러리 ── */
.gd-screenshots {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 40px;
}
.screenshot-card {
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
border: 1px solid var(--gray-200);
transition: all var(--mid) var(--ease);
}
.screenshot-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.screenshot-img {
width: 100%; aspect-ratio: 16/9;
object-fit: cover;
display: block;
}
.screenshot-placeholder {
width: 100%; aspect-ratio: 16/9;
background: linear-gradient(135deg, #1e2333, #2d3748);
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 8px; color: var(--gray-400); font-size: 13px;
}
.screenshot-placeholder .icon { font-size: 32px; }
.screenshot-caption {
padding: 12px 16px;
font-size: 13px; font-weight: 600; color: var(--gray-700);
background: var(--white);
}
/* ── 메신저 ── */
.messenger-platforms {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 48px;
}
.messenger-platform {
border-radius: var(--radius);
padding: 20px;
display: flex; align-items: center; gap: 14px;
}
.platform-icon { font-size: 28px; flex-shrink: 0; }
.messenger-platform strong { display: block; font-size: 15px; font-weight: 700; }
.messenger-platform p { font-size: 12px; margin-top: 2px; }
.cmd-catalog {
background: var(--gray-900);
border-radius: var(--radius-lg);
padding: 32px;
margin-bottom: 48px;
}
.cmd-catalog-title {
color: #fff; font-size: 18px; font-weight: 700;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255,255,255,.1);
}
.cmd-group { margin-bottom: 20px; }
.cmd-group-title {
font-size: 11px; font-weight: 700;
letter-spacing: 1.5px; text-transform: uppercase;
color: var(--accent); margin-bottom: 10px;
}
.cmd-list { display: flex; flex-direction: column; gap: 6px; }
.cmd-item { display: flex; align-items: baseline; gap: 16px; }
.cmd-code {
font-family: 'Courier New', monospace;
font-size: 13px; color: #a5f3fc;
background: rgba(165,243,252,.08);
padding: 3px 8px; border-radius: 4px;
white-space: nowrap; flex-shrink: 0;
min-width: 220px;
}
.cmd-desc { font-size: 13px; color: rgba(255,255,255,.65); }
/* 메신저 데모 */
.messenger-demo { }
.demo-title { font-size: 22px; font-weight: 700; color: var(--gray-900); margin-bottom: 24px; }
.demo-scenario { display: flex; flex-direction: column; gap: 32px; }
.demo-step { display: flex; gap: 20px; }
.step-num {
width: 40px; height: 40px; flex-shrink: 0;
background: var(--primary); color: #fff;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: 700;
}
.step-content { flex: 1; }
.step-content strong { font-size: 16px; color: var(--gray-900); display: block; margin-bottom: 6px; }
.step-content > p { font-size: 14px; color: var(--gray-600); margin-bottom: 10px; }
.chat-bubble {
padding: 12px 16px;
border-radius: 12px;
font-size: 13px; line-height: 1.6;
margin-top: 6px;
max-width: 520px;
}
.chat-bubble.bot { background: #1e2333; color: rgba(255,255,255,.9); }
.chat-bubble.user { background: var(--primary-light); color: var(--primary); font-weight: 600; }
/* ── 에디션 ── */
.gd-editions-grid {
display: grid; grid-template-columns: repeat(3,1fr);
gap: 24px;
}
.gd-edition-card {
border: 2px solid var(--gray-200);
border-radius: var(--radius-lg);
padding: 36px 28px;
position: relative;
display: flex; flex-direction: column; gap: 16px;
transition: all var(--mid) var(--ease);
}
.gd-edition-card:hover { box-shadow: var(--shadow-lg); }
.gd-edition-card.highlight {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(0,81,162,.1);
}
.edition-recommend {
position: absolute; top: -12px; left: 50%;
transform: translateX(-50%);
background: var(--primary); color: #fff;
font-size: 12px; font-weight: 700;
padding: 3px 14px; border-radius: 20px;
}
.edition-header { display: flex; align-items: center; justify-content: space-between; }
.edition-header h3 { font-size: 22px; font-weight: 900; color: var(--ed-color, var(--gray-900)); }
.edition-badge {
background: color-mix(in srgb, var(--ed-color, var(--primary)) 15%, transparent);
color: var(--ed-color, var(--primary));
padding: 3px 10px; border-radius: 12px;
font-size: 12px; font-weight: 700;
}
.edition-target { font-size: 13px; color: var(--gray-500); }
.edition-features { display: flex; flex-direction: column; gap: 10px; flex: 1; }
.edition-features li { display: flex; align-items: flex-start; gap: 10px; font-size: 14px; color: var(--gray-700); }
.check { color: var(--ed-color, var(--primary)); font-weight: 700; flex-shrink: 0; }
.edition-cta {
background: var(--ed-color, var(--primary));
color: #fff; text-align: center;
padding: 12px; border-radius: var(--radius);
font-weight: 600; width: 100%;
justify-content: center;
}
/* ── 기술 ── */
.gd-tech-grid {
display: grid; grid-template-columns: repeat(3,1fr); gap: 20px;
}
.gd-tech-card { padding: 28px; }
.tech-category { font-size: 16px; font-weight: 700; color: var(--primary); margin-bottom: 16px; }
.tech-items { display: flex; flex-direction: column; gap: 10px; }
.tech-items li {
font-size: 14px; color: var(--gray-700);
padding: 8px 12px;
background: var(--gray-50); border-radius: var(--radius-sm);
border-left: 3px solid var(--accent);
}
/* ── 도입 사례 ── */
.gd-usecases { display: grid; grid-template-columns: repeat(3,1fr); gap: 20px; }
.usecase-card { padding: 28px; display: flex; flex-direction: column; gap: 12px; }
.usecase-card h3 { font-size: 17px; font-weight: 700; color: var(--gray-900); }
.usecase-card p { font-size: 14px; color: var(--gray-600); line-height: 1.7; }
/* ── CTA ── */
.gd-cta {
background: linear-gradient(135deg, var(--primary-dark), #0a0f24);
padding: 80px 0; text-align: center;
}
.gd-cta h2 { font-size: 32px; font-weight: 800; color: #fff; margin-bottom: 12px; }
.gd-cta p { font-size: 17px; color: rgba(255,255,255,.75); margin-bottom: 32px; }
.gd-cta-actions { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
/* ── 반응형 ── */
@media (max-width: 1024px) {
.gd-hero-inner { grid-template-columns: 1fr; }
.gd-hero-stats { grid-template-columns: repeat(4,1fr); }
.gd-features-grid { grid-template-columns: repeat(2,1fr); }
.gd-screenshots { grid-template-columns: repeat(2,1fr); }
.messenger-platforms { grid-template-columns: repeat(2,1fr); }
.gd-editions-grid { grid-template-columns: 1fr; }
.gd-tech-grid { grid-template-columns: repeat(2,1fr); }
.gd-usecases { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.gd-features-grid { grid-template-columns: 1fr; }
.gd-screenshots { grid-template-columns: 1fr; }
.messenger-platforms { grid-template-columns: 1fr; }
.gd-hero-stats { grid-template-columns: repeat(2,1fr); }
.gd-tech-grid { grid-template-columns: 1fr; }
.cmd-item { flex-direction: column; gap: 4px; }
.cmd-code { min-width: unset; }
}

View File

@ -0,0 +1,405 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import './GuardiaDetail.css';
const FEATURES = [
{ icon:'🤖', title:'AI 에이전트 자동화',
desc:'Ollama 온프레미스 sLLM 기반. 메신저 한 줄 명령 → 자연어 파싱 → 자동 배포·운영. 외부 API 완전 차단으로 폐쇄망 환경 최적화.' },
{ icon:'🔧', title:'에이전트리스 아키텍처',
desc:'대상 서버에 어떤 소프트웨어도 설치하지 않습니다. 표준 SSH/SFTP 프로토콜만으로 레거시 WAS(Tomcat/JBoss/WebLogic)를 원격 관리.' },
{ icon:'💬', title:'ChatOps 메신저 통합',
desc:'카카오워크, 네이버웍스, 슬랙 등 익숙한 메신저에서 /deploy, /status, /incident 명령으로 인프라를 즉시 제어.' },
{ icon:'📊', title:'통합 ITSM 대시보드',
desc:'SR·인시던트·변경관리·SLA·CMDB·예측 유지보수를 단일 플랫폼에서 관리. 7일 추이 차트와 AI 인사이트 실시간 제공.' },
{ icon:'🔒', title:'엔터프라이즈 보안',
desc:'AES-256-GCM 암호화, MFA/OTP, PAM 특권접근관리, SHA-256 해시체인 불변 감사로그, Zero Trust 지속 인증.' },
{ icon:'🏗️', title:'PMS 프로젝트 관리',
desc:'WBS·산출물·일간/주간/월간 자동 보고서(Excel·PDF·PPT). 이슈·위험 관리, Gitea 연동, Jenkins CI/CD 파이프라인.' },
{ icon:'🌐', title:'공공기관 필수 준수',
desc:'행안부 SW 보안약점 자동 점검, KWCAG 2.1 웹접근성, 개인정보보호법 준수 스캔. 19개 공공기관 체크리스트 내장.' },
{ icon:'📡', title:'Scouter APM 모니터링',
desc:'Java WAS(Tomcat/JBoss) 전문 APM. CPU·Heap·TPS·응답시간 실시간 수집, 이상 탐지 시 자동 인시던트 생성.' },
];
const EDITIONS = [
{
name: 'COMMUNITY', badge: '무료',
color: '#10B981',
target: '소규모 기관·검토용',
features: ['기본 SR 관리 (무제한)', 'CMDB 서버 20대', '사용자 10명', '대시보드', '봇 기본 명령어'],
cta: '무료 시작',
href: '/support/contact?type=community',
},
{
name: 'STANDARD', badge: '권장',
color: 'var(--primary)',
target: '중형 기관',
features: ['전체 ITSM 기능', 'AI 에이전트 자동화', 'LDAP/AD 연동', 'MFA 보안', 'SLA 관리', 'PMS 기본'],
cta: '도입 문의',
href: '/support/contact?type=standard',
highlight: true,
},
{
name: 'ENTERPRISE', badge: '맞춤',
color: '#6366F1',
target: '대형 관공서·광역기관',
features: ['무제한 서버·기관', '취약점 자동 스캔', 'Scouter APM', 'FinOps 비용 분석', 'SIEM 연동', '전담 기술 지원'],
cta: '전문가 상담',
href: '/support/contact?type=enterprise',
},
];
/* ── 메신저 봇 명령어 목록 ──────────────────────────────── */
const BOT_COMMANDS = [
{ cmd: '/sr <제목>', desc: 'SR(서비스요청) 즉시 접수', cat: 'SR 관리' },
{ cmd: '/status', desc: '시스템 전체 현황 요약', cat: 'SR 관리' },
{ cmd: '/assign <SR-ID> <담당자>', desc: 'SR 담당자 즉시 배정', cat: 'SR 관리' },
{ cmd: '/approve <SR-ID>', desc: 'SR 즉시 승인', cat: 'SR 관리' },
{ cmd: '/sla', desc: 'SLA 위반 현황 목록', cat: 'SR 관리' },
{ cmd: '/incident <제목> [P1~P4]', desc: '인시던트 빠른 등록', cat: '인시던트' },
{ cmd: '/oncall', desc: '현재 당직자 즉시 조회', cat: '인시던트' },
{ cmd: '/rca <INC-ID>', desc: 'AI 자동 RCA 근본원인 분석', cat: '인시던트' },
{ cmd: '/escalate <SR-ID>', desc: '당직자에게 에스컬레이션', cat: '인시던트' },
{ cmd: '!deploy <세션ID>', desc: 'WAS 배포 실행 (SSH)', cat: '배포 제어' },
{ cmd: '/rollback <세션ID>', desc: '긴급 롤백', cat: '배포 제어' },
{ cmd: '!health <서버명>', desc: '서버 헬스체크', cat: '배포 제어' },
{ cmd: '/pms <프로젝트코드>', desc: '프로젝트 진척 현황', cat: 'PMS' },
{ cmd: '/report <코드> weekly', desc: '주간 보고서 메신저 발송', cat: 'PMS' },
{ cmd: '/wbs <코드>', desc: 'WBS 지연 현황', cat: 'PMS' },
{ cmd: '/scouter <서버명>', desc: 'Scouter APM 실시간 메트릭', cat: '모니터링' },
{ cmd: '/scan', desc: '시큐어코딩·보안 자동 점검', cat: '보안' },
{ cmd: '/vuln <서버|IP>', desc: '취약점 스캔', cat: '보안' },
{ cmd: '/notify <메시지>', desc: '운영팀 전체 공지 발송', cat: '공지' },
];
const MESSENGER_PLATFORMS = [
{ name: '카카오워크', icon: '💬', color: '#FAE100', textColor: '#3C1E1E', desc: '결재 버튼 + 봇 명령 완벽 지원' },
{ name: '네이버웍스', icon: '🟢', color: '#03C75A', textColor: '#fff', desc: 'Flex 메시지 + Rich 결과 표시' },
{ name: '슬랙', icon: '💜', color: '#611F69', textColor: '#fff', desc: '슬래시 명령 + 블록킷 UI' },
{ name: '자체 메신저', icon: '🔵', color: '#0051A2', textColor: '#fff', desc: 'GUARDiA 내장 Slack형 메신저' },
];
const TECH_STACK = [
{ category: 'Backend', items: ['Python 3.11 / FastAPI', 'SQLAlchemy Async', 'PostgreSQL / SQLite'] },
{ category: 'AI·LLM', items: ['Ollama (온프레미스)', 'llama3.1:8b / codellama', '외부 API 완전 차단'] },
{ category: 'Infra', items: ['paramiko SSH/SFTP', '에이전트리스', 'AES-256-GCM 암호화'] },
{ category: 'Frontend', items: ['React.js / PWA', 'Chart.js 대시보드', 'D3.js 토폴로지'] },
{ category: 'DevOps', items: ['Jenkins CI/CD', 'Gitea 형상관리', 'Docker / K8s'] },
{ category: '모니터링', items: ['Scouter APM', 'Prometheus/Grafana', 'ELK/Splunk SIEM'] },
];
export default function GuardiaDetail() {
const [activeTab, setActiveTab] = useState('features');
return (
<main id="main-content" className="guardia-page">
{/* ── 히어로 ────────────────────────────────────────── */}
<section className="gd-hero">
<div className="gd-hero-overlay" />
<div className="container gd-hero-inner">
<div className="gd-hero-text">
<span className="badge badge-new" style={{fontSize:'12px',padding:'4px 12px'}}>NEW v2.0</span>
<h1 className="gd-hero-title">
<span>GUARDiA</span> ITSM
</h1>
<p className="gd-hero-sub">
AI 기반 레거시 인프라 자율 운영 플랫폼<br/>
<strong>메신저 명령</strong>으로 1,000 관공서 인프라를 자동화
</p>
<div className="gd-hero-actions">
<Link to="/support/contact?type=demo" className="btn btn-white btn-lg">
무료 데모 신청
</Link>
<a href="#features" className="btn btn-lg" style={{color:'rgba(255,255,255,.85)',border:'1px solid rgba(255,255,255,.3)'}}>
기능 살펴보기
</a>
</div>
</div>
<div className="gd-hero-stats">
{[
{val:'1,000+', lab:'관리 기관'},
{val:'99.9%', lab:'가용성'},
{val:'70%', lab:'운영 비용 절감'},
{val:'0개', lab:'대상 서버 추가 설치'},
].map((s,i) => (
<div key={i} className="gd-stat">
<div className="gd-stat-val">{s.val}</div>
<div className="gd-stat-lab">{s.lab}</div>
</div>
))}
</div>
</div>
</section>
{/* ── 탭 메뉴 ──────────────────────────────────────── */}
<div className="gd-tabs-bar">
<div className="container">
<div className="gd-tabs">
{[
{id:'features', label:'핵심 기능'},
{id:'messenger', label:'Messenger Bot'},
{id:'editions', label:'에디션 비교'},
{id:'tech', label:'기술 스택'},
{id:'usecase', label:'도입 사례'},
].map(t => (
<button key={t.id}
className={`gd-tab ${activeTab === t.id ? 'active' : ''}`}
onClick={() => setActiveTab(t.id)}>
{t.label}
</button>
))}
</div>
</div>
</div>
{/* ── 핵심 기능 ─────────────────────────────────────── */}
{activeTab === 'features' && (
<section id="features" className="section">
<div className="container">
<div className="section-header">
<span className="section-label">Core Features</span>
<h2 className="section-title">GUARDiA가 제공하는<br/><em>8가지 핵심 기능</em></h2>
<div className="divider" />
</div>
{/* 실행 화면 스크린샷 */}
<div className="gd-screenshots">
{[
{file:'01_dashboard', caption:'통합 대시보드 — SR·SLA·AI 인사이트'},
{file:'02_sr_list', caption:'SR 서비스 요청 — 칸반/목록 뷰'},
{file:'03_si_project', caption:'PMS 프로젝트 — WBS·산출물·보고서'},
{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}
className="screenshot-img"
onError={e=>{e.target.style.display='none';e.target.nextSibling.style.display='flex';}}/>
<div className="screenshot-placeholder" style={{display:'none'}}>
<span className="icon">🖥</span><span>준비 </span>
</div>
<div className="screenshot-caption">{s.caption}</div>
</div>
))}
</div>
<div className="section-header" style={{marginTop:'60px',marginBottom:'32px'}}>
<h3 style={{fontSize:'28px',fontWeight:'800',color:'var(--gray-900)'}}>핵심 기능 상세</h3>
</div>
<div className="gd-features-grid">
{FEATURES.map((f,i) => (
<div key={i} className="gd-feature-card card">
<div className="gd-feature-icon">{f.icon}</div>
<h3>{f.title}</h3>
<p>{f.desc}</p>
</div>
))}
</div>
</div>
</section>
)}
{/* ── 메신저 봇 상세 소개 ──────────────────────────── */}
{activeTab === 'messenger' && (
<section className="section">
<div className="container">
<div className="section-header">
<span className="section-label">ChatOps Messenger</span>
<h2 className="section-title">메신저 하나로<br/><em>모든 인프라를 제어</em></h2>
<div className="divider" />
<p className="section-desc">
익숙한 메신저에서 명령어 하나로 서버 배포·장애 대응·보고서 발송까지.<br/>
GUARDiA Bot은 25 명령어로 IT 운영의 모든 순간을 지원합니다.
</p>
</div>
{/* 지원 플랫폼 */}
<div className="messenger-platforms">
{MESSENGER_PLATFORMS.map((p,i) => (
<div key={i} className="messenger-platform" style={{background: p.color}}>
<span className="platform-icon">{p.icon}</span>
<div>
<strong style={{color: p.textColor}}>{p.name}</strong>
<p style={{color: p.textColor, opacity:.85}}>{p.desc}</p>
</div>
</div>
))}
</div>
{/* 명령어 카탈로그 */}
<div className="cmd-catalog">
<h3 className="cmd-catalog-title">25 명령어 전체 목록</h3>
{['SR 관리','인시던트','배포 제어','PMS','모니터링','보안','공지'].map(cat => {
const cmds = BOT_COMMANDS.filter(c => c.cat === cat);
return (
<div key={cat} className="cmd-group">
<h4 className="cmd-group-title">{cat}</h4>
<div className="cmd-list">
{cmds.map((c,i) => (
<div key={i} className="cmd-item">
<code className="cmd-code">{c.cmd}</code>
<span className="cmd-desc">{c.desc}</span>
</div>
))}
</div>
</div>
);
})}
</div>
{/* 데모 시나리오 */}
<div className="messenger-demo">
<h3 className="demo-title">실제 운영 시나리오</h3>
<div className="demo-scenario">
<div className="demo-step">
<div className="step-num">1</div>
<div className="step-content">
<strong>장애 탐지</strong>
<p>Scouter APM이 서버 CPU 90% 감지 자동으로 GUARDiA 운영 채널에 경보 발송</p>
<div className="chat-bubble bot">🚨 web-01 CPU 90.3% P2 인시던트 자동 등록: INC-20260530-A1B2C3</div>
</div>
</div>
<div className="demo-step">
<div className="step-num">2</div>
<div className="step-content">
<strong>담당자 즉시 대응</strong>
<p>메신저에서 RCA 분석 요청</p>
<div className="chat-bubble user">/rca INC-20260530-A1B2C3</div>
<div className="chat-bubble bot">
🤖 AI RCA 분석 완료<br/>
근본원인: 메모리 누수 (Heap 98%)<br/>
재발방지: WAS 재기동 + 힙덤프 분석<br/>
신뢰도: 87%
</div>
</div>
</div>
<div className="demo-step">
<div className="step-num">3</div>
<div className="step-content">
<strong>원격 조치 실행</strong>
<p>SSH 재기동 명령 실행</p>
<div className="chat-bubble user">!sm web-01 tomcat_restart</div>
<div className="chat-bubble bot">
web-01 Tomcat 재기동 완료<br/>
소요: 38 | CPU: 12% | 정상화
</div>
</div>
</div>
<div className="demo-step">
<div className="step-num">4</div>
<div className="step-content">
<strong>자동 보고</strong>
<p>인시던트 처리 결과 자동 보고서 발송</p>
<div className="chat-bubble user">/notify 22:15 web-01 서버 장애 복구 완료. 원인: 메모리 누수 재발 방지 조치 완료.</div>
<div className="chat-bubble bot"> 운영팀 전체 공지 발송 완료 (ops 채널)</div>
</div>
</div>
</div>
</div>
</div>
</section>
)}
{/* ── 에디션 비교 ──────────────────────────────────── */}
{activeTab === 'editions' && (
<section className="section">
<div className="container">
<div className="section-header">
<span className="section-label">Editions</span>
<h2 className="section-title">기관 규모에 맞는<br/><em>에디션 선택</em></h2>
<div className="divider" />
</div>
<div className="gd-editions-grid">
{EDITIONS.map((e,i) => (
<div key={i} className={`gd-edition-card ${e.highlight ? 'highlight' : ''}`}
style={{'--ed-color': e.color}}>
{e.highlight && <div className="edition-recommend">추천</div>}
<div className="edition-header">
<h3>{e.name}</h3>
<span className="edition-badge">{e.badge}</span>
</div>
<p className="edition-target">{e.target}</p>
<ul className="edition-features">
{e.features.map((f,j) => (
<li key={j}><span className="check"></span>{f}</li>
))}
</ul>
<Link to={e.href} className="btn edition-cta">
{e.cta}
</Link>
</div>
))}
</div>
</div>
</section>
)}
{/* ── 기술 스택 ─────────────────────────────────────── */}
{activeTab === 'tech' && (
<section className="section">
<div className="container">
<div className="section-header">
<span className="section-label">Technology</span>
<h2 className="section-title">검증된<br/><em>기술 스택</em></h2>
<div className="divider" />
<p className="section-desc">온프레미스 전용 설계 외부 클라우드 의존 없는 완전 폐쇄망 동작</p>
</div>
<div className="gd-tech-grid">
{TECH_STACK.map((t,i) => (
<div key={i} className="gd-tech-card card">
<h3 className="tech-category">{t.category}</h3>
<ul className="tech-items">
{t.items.map((item,j) => (
<li key={j}>{item}</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
)}
{/* ── 도입 사례 ─────────────────────────────────────── */}
{activeTab === 'usecase' && (
<section className="section">
<div className="container">
<div className="section-header">
<span className="section-label">Use Cases</span>
<h2 className="section-title">실제 <em>도입 사례</em></h2>
<div className="divider" />
</div>
<div className="gd-usecases">
{[
{org:'광역 지방자치단체', result:'레거시 서버 200대 SSH 자동화, 운영 인력 3명→1명', badge:'중앙부처'},
{org:'공공기관 IT센터', result:'월간 SR 500건 처리, AI 자동분류로 80% 응답시간 단축', badge:'공공기관'},
{org:'지방 교육청', result:'Tomcat 100대 무중단 배포 자동화, 장애 대응 시간 70% 단축', badge:'교육'},
].map((u,i) => (
<div key={i} className="usecase-card card">
<span className="badge badge-primary">{u.badge}</span>
<h3>{u.org}</h3>
<p>{u.result}</p>
</div>
))}
</div>
</div>
</section>
)}
{/* ── CTA ─────────────────────────────────────────────── */}
<section className="gd-cta">
<div className="container">
<h2>지금 바로 무료 데모를 경험해 보세요</h2>
<p>전문 컨설턴트가 기관 환경에 맞는 최적의 구성을 제안해 드립니다.</p>
<div className="gd-cta-actions">
<Link to="/support/contact?type=demo" className="btn btn-white btn-lg">무료 데모 신청</Link>
<Link to="/support/catalog" className="btn btn-lg" style={{color:'rgba(255,255,255,.8)',border:'1px solid rgba(255,255,255,.3)'}}>제품 소개서</Link>
</div>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,303 @@
/* ─── 히어로 ──────────────────────────────────────────────── */
.hero {
position: relative;
height: 100vh; min-height: 600px;
display: flex; align-items: center;
overflow: hidden;
background: linear-gradient(135deg, #0a0f24 0%, #0d2463 50%, #0a1a4a 100%);
transition: background var(--slow);
}
.hero-0 { background: linear-gradient(135deg, #0a0f24 0%, #0d2463 60%, #0051A2 100%); }
.hero-1 { background: linear-gradient(135deg, #1a0f3a 0%, #2d1b69 60%, #0051A2 100%); }
.hero-2 { background: linear-gradient(135deg, #001a3a 0%, #003070 60%, #00A3E0 100%); }
/* 애니메이션 배경 패턴 */
.hero::before {
content: '';
position: absolute; inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.hero-overlay {
position: absolute; inset: 0;
background: linear-gradient(to right, rgba(0,0,0,.5) 0%, rgba(0,0,0,.1) 100%);
}
.hero-content {
position: relative; z-index: 2;
max-width: 700px;
}
.hero-badge {
display: inline-block;
background: var(--accent);
color: #fff;
font-size: 12px; font-weight: 700;
letter-spacing: 2px;
padding: 4px 12px; border-radius: 20px;
margin-bottom: 20px;
animation: fadeUp .5s ease;
}
.hero-title {
font-size: clamp(36px, 5.5vw, 64px);
font-weight: 900;
color: #fff;
line-height: 1.15;
margin-bottom: 20px;
animation: fadeUp .5s .1s ease both;
}
.hero-sub {
font-size: clamp(16px, 2vw, 20px);
color: rgba(255,255,255,.8);
margin-bottom: 40px;
line-height: 1.6;
animation: fadeUp .5s .2s ease both;
}
.hero-actions {
display: flex; gap: 16px; flex-wrap: wrap;
animation: fadeUp .5s .3s ease both;
}
.hero-btn-contact {
border-color: rgba(255,255,255,.5);
color: #fff !important;
}
.hero-btn-contact:hover {
background: rgba(255,255,255,.15) !important;
color: #fff !important;
}
/* 컨트롤 */
.hero-controls {
position: absolute; bottom: 40px; left: 50%;
transform: translateX(-50%);
display: flex; align-items: center; gap: 12px;
z-index: 3;
}
.hero-arrow {
width: 36px; height: 36px;
border-radius: 50%;
background: rgba(255,255,255,.15);
color: #fff;
font-size: 20px;
display: flex; align-items: center; justify-content: center;
transition: background var(--fast);
}
.hero-arrow:hover { background: rgba(255,255,255,.3); }
.hero-dots { display: flex; gap: 8px; }
.hero-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: rgba(255,255,255,.4);
transition: all var(--mid);
}
.hero-dot.active {
background: #fff; width: 24px; border-radius: 4px;
}
.hero-pause {
width: 32px; height: 32px;
border-radius: 50%;
background: rgba(255,255,255,.1);
color: rgba(255,255,255,.7);
font-size: 12px;
display: flex; align-items: center; justify-content: center;
}
/* 스크롤 힌트 */
.hero-scroll-hint {
position: absolute; right: 40px; bottom: 40px;
display: flex; flex-direction: column; align-items: center; gap: 8px;
color: rgba(255,255,255,.5); font-size: 10px; letter-spacing: 2px;
}
.scroll-line {
width: 1px; height: 60px;
background: linear-gradient(to bottom, rgba(255,255,255,.5), transparent);
animation: scrollPulse 1.5s ease infinite;
}
@keyframes scrollPulse {
0%,100% { opacity:.5; transform:scaleY(1); }
50% { opacity:1; transform:scaleY(1.1); }
}
/* ─── 핵심 사업 ──────────────────────────────────────────── */
.business-grid {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 28px;
}
.business-card {
background: var(--white);
border-radius: var(--radius-lg);
padding: 40px 32px;
border: 1px solid var(--gray-200);
transition: all var(--mid) var(--ease);
display: flex; flex-direction: column; gap: 12px;
}
.business-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-6px);
border-color: transparent;
}
.business-icon {
width: 64px; height: 64px;
border-radius: var(--radius);
font-size: 28px;
display: flex; align-items: center; justify-content: center;
}
.business-title { font-size: 22px; font-weight: 700; color: var(--gray-900); }
.business-desc { font-size: 15px; color: var(--gray-600); line-height: 1.7; flex: 1; }
.business-more { font-size: 14px; font-weight: 600; }
/* ─── GUARDiA 섹션 ───────────────────────────────────────── */
.section-guardia { background: var(--gray-50); }
.guardia-inner {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 80px;
align-items: center;
}
.guardia-desc {
font-size: 16px; color: var(--gray-600);
line-height: 1.8; margin: 20px 0 28px;
}
.guardia-features {
display: flex; flex-direction: column; gap: 16px;
margin-bottom: 32px;
}
.guardia-feature {
display: flex; align-items: flex-start; gap: 16px;
padding: 14px 18px;
background: var(--white);
border-radius: var(--radius);
border: 1px solid var(--gray-200);
}
.feature-icon { font-size: 24px; flex-shrink: 0; }
.guardia-feature strong { display: block; font-size: 15px; color: var(--gray-900); }
.guardia-feature p { font-size: 13px; color: var(--gray-600); margin-top: 2px; }
.guardia-actions { display: flex; gap: 12px; flex-wrap: wrap; }
/* 모의 채팅 UI */
.guardia-mockup {
background: #1e1e2e;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-lg);
border: 1px solid rgba(255,255,255,.1);
}
.mockup-bar {
background: #2d2d3f;
padding: 10px 16px;
display: flex; gap: 6px;
}
.mockup-bar span {
width: 12px; height: 12px;
border-radius: 50%;
background: #ff5f56;
}
.mockup-bar span:nth-child(2) { background: #ffbd2e; }
.mockup-bar span:nth-child(3) { background: #27c93f; }
.mockup-content { padding: 20px; }
.mockup-chat { display: flex; flex-direction: column; gap: 12px; }
.chat-msg { max-width: 80%; }
.chat-msg.bot { align-self: flex-start; }
.chat-msg.user { align-self: flex-end; }
.chat-name { font-size: 11px; color: var(--accent); font-weight: 700; display: block; margin-bottom: 4px; }
.chat-msg p {
padding: 10px 14px;
border-radius: 12px;
font-size: 13px; line-height: 1.5;
}
.chat-msg.bot p { background: #2d2d3f; color: rgba(255,255,255,.9); }
.chat-msg.user p { background: var(--primary); color: #fff; }
/* ─── KPI ────────────────────────────────────────────────── */
.section-kpi {
background: var(--primary);
padding: 60px 0;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: 0;
}
.kpi-item {
text-align: center;
padding: 24px;
border-right: 1px solid rgba(255,255,255,.2);
}
.kpi-item:last-child { border-right: none; }
.kpi-value {
font-size: clamp(32px, 4vw, 48px);
font-weight: 900;
color: #fff;
}
.kpi-label {
font-size: 14px; color: rgba(255,255,255,.75);
margin-top: 6px;
}
/* ─── 소식 ───────────────────────────────────────────────── */
.news-grid {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: 20px;
}
.news-card { display: flex; flex-direction: column; }
.news-card-body { padding: 24px; flex: 1; display: flex; flex-direction: column; gap: 10px; }
.news-title {
font-size: 16px; font-weight: 700; color: var(--gray-900);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-summary {
font-size: 13px; color: var(--gray-600);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.news-card-footer {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 24px;
border-top: 1px solid var(--gray-100);
font-size: 12px;
}
.news-date { color: var(--gray-400); }
.news-more { color: var(--primary); font-weight: 600; }
/* 스켈레톤 */
.skeleton { animation: pulse 1.5s ease infinite; }
@keyframes pulse { 0%,100%{opacity:.6} 50%{opacity:1} }
.skel-line { background: var(--gray-200); border-radius: 4px; }
/* ─── CTA ────────────────────────────────────────────────── */
.section-cta {
background: linear-gradient(135deg, var(--secondary), var(--primary-dark));
padding: 80px 0;
}
.cta-inner {
display: flex; align-items: center; justify-content: space-between;
gap: 40px; flex-wrap: wrap;
}
.cta-text h2 { font-size: 28px; font-weight: 800; color: #fff; }
.cta-text p { color: rgba(255,255,255,.75); margin-top: 8px; font-size: 16px; }
.cta-actions { display: flex; gap: 16px; flex-wrap: wrap; flex-shrink: 0; }
/* ─── 반응형 ──────────────────────────────────────────────── */
@media (max-width: 1024px) {
.business-grid { grid-template-columns: repeat(2,1fr); }
.guardia-inner { grid-template-columns: 1fr; gap: 40px; }
.kpi-grid { grid-template-columns: repeat(2,1fr); }
.news-grid { grid-template-columns: repeat(2,1fr); }
}
@media (max-width: 768px) {
.hero-title { font-size: 32px; }
.hero-sub { font-size: 15px; }
.hero-scroll-hint { display: none; }
.business-grid { grid-template-columns: 1fr; }
.kpi-grid { grid-template-columns: repeat(2,1fr); }
.news-grid { grid-template-columns: 1fr; }
.cta-inner { flex-direction: column; text-align: center; }
.cta-actions { justify-content: center; }
}

View File

@ -0,0 +1,315 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import './Home.css';
/* ── 히어로 슬라이드 데이터 ─────────────────────────────── */
const SLIDES = [
{
title: 'AI 기반 인프라\n자율 운영 플랫폼',
sub: 'GUARDiA ITSM — 메신저 한 줄로 1,000개 관공서 인프라를 자동화',
cta: { label: 'GUARDiA 알아보기', path: '/solution/guardia' },
badge: 'NEW',
bg: 'slide-1',
},
{
title: '공공기관 전문\nIT 솔루션 기업',
sub: '20년 경험의 지오정보기술이 최첨단 AI 기술로 여러분과 함께합니다',
cta: { label: '회사소개 보기', path: '/company/greeting' },
badge: '',
bg: 'slide-2',
},
{
title: '에이전트리스\n자동화 혁신',
sub: '대상 서버에 소프트웨어 설치 없이 SSH만으로 레거시 인프라를 관리',
cta: { label: '도입 문의', path: '/support/contact' },
badge: '',
bg: 'slide-3',
},
];
/* ── 핵심 사업 영역 ──────────────────────────────────────── */
const BUSINESS = [
{
icon: '🤖',
title: 'AI 자동화',
desc: 'GUARDiA ITSM 플랫폼으로 레거시 인프라 운영을 완전 자동화',
path: '/solution/guardia',
color: 'var(--primary)',
},
{
icon: '🏗️',
title: 'SI 구축',
desc: '공공기관 정보화사업 시스템 통합 및 맞춤형 개발',
path: '/business/reference',
color: 'var(--accent)',
},
{
icon: '💼',
title: 'ERP·CRM·BI',
desc: '기업 경영 효율화를 위한 통합 솔루션 패키지',
path: '/solution/erp',
color: '#10B981',
},
];
/* ── GUARDiA 핵심 기능 ───────────────────────────────────── */
const GUARDIA_FEATURES = [
{ icon: '💬', label: 'ChatOps', desc: '메신저 명령으로 인프라 제어' },
{ icon: '🔧', label: '에이전트리스', desc: 'SSH만으로 에이전트 설치 없음' },
{ icon: '📊', label: '통합 ITSM', desc: 'SR·인시던트·변경·SLA 통합' },
{ icon: '🔒', label: '엔터프라이즈 보안', desc: 'MFA·PAM·Zero Trust' },
];
/* ── KPI 수치 ────────────────────────────────────────────── */
const KPIS = [
{ value: '1,000+', label: '관리 가능 기관 수' },
{ value: '99.9%', label: '시스템 가용성' },
{ value: '70%', label: 'SR 처리 시간 단축' },
{ value: '20년+', label: 'IT 사업 경험' },
];
export default function Home() {
const [slide, setSlide] = useState(0);
const [paused, setPaused] = useState(false);
const [news, setNews] = useState([]);
const timerRef = useRef(null);
/* 슬라이드 자동 전환 */
useEffect(() => {
if (paused) return;
timerRef.current = setInterval(() => setSlide(s => (s + 1) % SLIDES.length), 5000);
return () => clearInterval(timerRef.current);
}, [paused]);
/* 소식 로드 */
useEffect(() => {
axios.get('/api/news?size=4').then(r => setNews(r.data.content || [])).catch(() => {});
}, []);
const prevSlide = () => setSlide(s => (s - 1 + SLIDES.length) % SLIDES.length);
const nextSlide = () => setSlide(s => (s + 1) % SLIDES.length);
const current = SLIDES[slide];
return (
<main id="main-content">
{/* ── 히어로 슬라이더 ──────────────────────────────── */}
<section className={`hero hero-${slide}`}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
aria-label="메인 슬라이더">
<div className="hero-overlay" />
<div className="hero-content container">
{current.badge && (
<span className="hero-badge">{current.badge}</span>
)}
<h1 className="hero-title">
{current.title.split('\n').map((line, i) => (
<React.Fragment key={i}>{line}{i < current.title.split('\n').length - 1 && <br/>}</React.Fragment>
))}
</h1>
<p className="hero-sub">{current.sub}</p>
<div className="hero-actions">
<Link to={current.cta.path} className="btn btn-white btn-lg">
{current.cta.label}
</Link>
<Link to="/support/contact" className="btn btn-outline hero-btn-contact btn-lg">
무료 상담
</Link>
</div>
</div>
{/* 슬라이드 컨트롤 */}
<div className="hero-controls">
<button onClick={prevSlide} aria-label="이전 슬라이드" className="hero-arrow"></button>
<div className="hero-dots">
{SLIDES.map((_, i) => (
<button key={i} className={`hero-dot ${i === slide ? 'active' : ''}`}
onClick={() => setSlide(i)}
aria-label={`슬라이드 ${i + 1}`} />
))}
</div>
<button onClick={nextSlide} aria-label="다음 슬라이드" className="hero-arrow"></button>
<button onClick={() => setPaused(v => !v)} className="hero-pause" aria-label={paused ? '재생' : '일시정지'}>
{paused ? '▶' : '⏸'}
</button>
</div>
{/* 스크롤 유도 */}
<div className="hero-scroll-hint" aria-hidden="true">
<span>SCROLL</span>
<div className="scroll-line" />
</div>
</section>
{/* ── 핵심 사업 영역 ───────────────────────────────── */}
<section className="section section-business">
<div className="container">
<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="business-grid">
{BUSINESS.map((b, i) => (
<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 솔루션 하이라이트 ──────────────────────── */}
<section className="section section-guardia">
<div className="container">
<div className="guardia-inner">
<div className="guardia-text">
<span className="section-label">대표 솔루션</span>
<h2 className="section-title" style={{textAlign:'left'}}>
<em>GUARDiA ITSM</em><br/>
AI 기반 인프라 자율 운영
</h2>
<div className="divider divider-left" />
<p className="guardia-desc">
1,000 이상의 관공서 레거시 인프라를 메신저 명령으로 제어하는
온프레미스 AI ChatOps 플랫폼. 에이전트 설치 없이 SSH/SFTP만으로
배포·운영을 완전 자동화합니다.
</p>
<div className="guardia-features">
{GUARDIA_FEATURES.map((f, i) => (
<div key={i} className="guardia-feature">
<span className="feature-icon">{f.icon}</span>
<div>
<strong>{f.label}</strong>
<p>{f.desc}</p>
</div>
</div>
))}
</div>
<div className="guardia-actions">
<Link to="/solution/guardia" className="btn btn-primary">
GUARDiA 상세보기
</Link>
<Link to="/support/contact" className="btn btn-outline">
도입 문의
</Link>
</div>
</div>
<div className="guardia-visual">
<div className="guardia-mockup">
<div className="mockup-bar">
<span/><span/><span/>
</div>
<div className="mockup-content">
<div className="mockup-chat">
<div className="chat-msg bot">
<span className="chat-name">GUARDiA Bot</span>
<p>안녕하세요! 무엇을 도와드릴까요?</p>
</div>
<div className="chat-msg user">
<p>/deploy web-server-01</p>
</div>
<div className="chat-msg bot">
<p> web-server-01 배포 완료<br/>헬스체크: 정상 | 소요: 42</p>
</div>
<div className="chat-msg user">
<p>/status</p>
</div>
<div className="chat-msg bot">
<p>📊 시스템 현황<br/>SR 처리중: 3 | SLA 준수율: 98.2%<br/>서버 이상: 0 </p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* ── KPI 수치 ────────────────────────────────────────── */}
<section className="section-kpi">
<div className="container">
<div className="kpi-grid">
{KPIS.map((k, i) => (
<div key={i} className="kpi-item">
<div className="kpi-value">{k.value}</div>
<div className="kpi-label">{k.label}</div>
</div>
))}
</div>
</div>
</section>
{/* ── 최신 소식 ───────────────────────────────────────── */}
<section className="section section-news">
<div className="container">
<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 => (
<Link to={`/news/newsroom/${n.id}`} key={n.id} className="news-card card">
<div className="news-card-body">
<span className={`badge badge-accent`}>{n.category}</span>
<h3 className="news-title">{n.title}</h3>
<p className="news-summary">{n.summary}</p>
</div>
<div className="news-card-footer">
<span className="news-date">
{n.createdAt ? new Date(n.createdAt).toLocaleDateString('ko-KR') : ''}
</span>
<span className="news-more">더보기 </span>
</div>
</Link>
)) : (
/* 스켈레톤 */
Array.from({length: 4}).map((_, i) => (
<div key={i} className="news-card card skeleton">
<div className="skel-line" style={{width:'30%',height:'16px'}} />
<div className="skel-line" style={{width:'90%',height:'20px',marginTop:'8px'}} />
<div className="skel-line" style={{width:'70%',height:'14px',marginTop:'6px'}} />
</div>
))
)}
</div>
<div style={{textAlign:'center',marginTop:'40px'}}>
<Link to="/news/newsroom" className="btn btn-outline">
모든 소식 보기
</Link>
</div>
</div>
</section>
{/* ── CTA 배너 ────────────────────────────────────────── */}
<section className="section-cta">
<div className="container">
<div className="cta-inner">
<div className="cta-text">
<h2>GUARDiA ITSM 도입을 검토하고 계신가요?</h2>
<p>전문 컨설턴트가 기관 환경에 맞는 최적의 방안을 제안해 드립니다.</p>
</div>
<div className="cta-actions">
<Link to="/support/contact" className="btn btn-white btn-lg">
무료 상담 신청
</Link>
<Link to="/solution/guardia" className="btn btn-outline btn-lg" style={{color:'#fff',borderColor:'rgba(255,255,255,.5)'}}>
제품 소개서 다운로드
</Link>
</div>
</div>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,35 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import axios from "axios";
import "./Common.css";
export default function NewsPage() {
const [news, setNews] = useState([]);
useEffect(() => { axios.get("/api/news?size=12").then(r => setNews(r.data.content || [])); }, []);
return (
<main id="main-content" className="inner-page">
<div className="page-hero">
<div className="container">
<span className="section-label">News</span>
<h1 className="page-hero-title">뉴스룸</h1>
</div>
</div>
<section className="section">
<div className="container">
<div className="grid-3">
{news.map(n => (
<div key={n.id} className="card" style={{padding:"24px",display:"flex",flexDirection:"column",gap:"10px"}}>
<span className="badge badge-accent">{n.category}</span>
<h3 style={{fontSize:"16px",fontWeight:"700"}}>{n.title}</h3>
<p style={{fontSize:"13px",color:"var(--gray-600)",flex:1}}>{n.summary}</p>
<span style={{fontSize:"12px",color:"var(--gray-400)"}}>
{n.createdAt ? new Date(n.createdAt).toLocaleDateString("ko-KR") : ""}
</span>
</div>
))}
</div>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,13 @@
import React from "react";
import { Link } from "react-router-dom";
export default function NotFound() {
return (
<main style={{paddingTop:"var(--header-h)",minHeight:"60vh",display:"flex",alignItems:"center",justifyContent:"center",flexDirection:"column",gap:"16px",textAlign:"center"}}>
<div style={{fontSize:"72px"}}>404</div>
<h1 style={{fontSize:"24px",fontWeight:"700"}}>페이지를 찾을 없습니다</h1>
<p style={{color:"var(--gray-600)"}}>요청하신 페이지가 존재하지 않거나 이동되었습니다.</p>
<Link to="/" className="btn btn-primary">홈으로 돌아가기</Link>
</main>
);
}

View File

@ -0,0 +1,42 @@
import React from "react";
import "./Common.css";
const JOBS = [
{ title:"Spring Boot 백엔드 개발자", dept:"개발팀", type:"정규직", exp:"경력 3년 이상" },
{ title:"React 프론트엔드 개발자", dept:"개발팀", type:"정규직", exp:"경력 2년 이상" },
{ title:"AI/ML 엔지니어", dept:"AI팀", type:"정규직", exp:"경력 3년 이상" },
{ title:"인프라 운영 엔지니어", dept:"운영팀", type:"정규직", exp:"경력 2년 이상" },
];
export default function Recruit() {
return (
<main id="main-content" className="inner-page">
<div className="page-hero">
<div className="container">
<span className="section-label">Recruit</span>
<h1 className="page-hero-title">채용 공고</h1>
<p>지오정보기술과 함께 AI 인프라 혁신을 이끌어 인재를 모십니다.</p>
</div>
</div>
<section className="section">
<div className="container">
<div style={{display:"flex",flexDirection:"column",gap:"16px"}}>
{JOBS.map((j,i) => (
<div key={i} className="card" style={{padding:"24px",display:"flex",alignItems:"center",gap:"20px"}}>
<div style={{flex:1}}>
<h3 style={{fontSize:"18px",fontWeight:"700"}}>{j.title}</h3>
<div style={{display:"flex",gap:"12px",marginTop:"8px"}}>
<span className="badge badge-primary">{j.dept}</span>
<span className="badge badge-accent">{j.type}</span>
<span style={{fontSize:"13px",color:"var(--gray-500)"}}>{j.exp}</span>
</div>
</div>
<a href="/support/contact?type=채용문의" className="btn btn-primary">지원하기</a>
</div>
))}
</div>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,215 @@
/* ============================================================
()지오정보기술 글로벌 스타일 URP 스타일 기반
============================================================ */
:root {
/* ── 브랜드 컬러 ── */
--primary: #0051A2; /* 딥블루 — 메인 브랜드 */
--primary-dark: #003A7A;
--primary-light: #E8F0FA;
--accent: #00A3E0; /* 밝은 파랑 — 포인트 */
--accent-dark: #0080B0;
--secondary: #1A1A2E; /* 다크 네이비 — 헤더 배경 */
/* ── 그레이 스케일 ── */
--gray-900: #111827;
--gray-800: #1F2937;
--gray-700: #374151;
--gray-600: #4B5563;
--gray-400: #9CA3AF;
--gray-200: #E5E7EB;
--gray-100: #F3F4F6;
--gray-50: #F9FAFB;
--white: #FFFFFF;
/* ── 시맨틱 ── */
--success: #10B981;
--warning: #F59E0B;
--danger: #EF4444;
/* ── 타이포그래피 ── */
--font-sans: 'Noto Sans KR', 'Inter', -apple-system, sans-serif;
--font-en: 'Inter', sans-serif;
/* ── 레이아웃 ── */
--container: 1280px;
--header-h: 72px;
--radius-sm: 6px;
--radius: 12px;
--radius-lg: 20px;
/* ── 트랜지션 ── */
--ease: cubic-bezier(.4,0,.2,1);
--fast: 0.15s;
--mid: 0.3s;
--slow: 0.5s;
/* ── 그림자 ── */
--shadow-sm: 0 1px 3px rgba(0,0,0,.1);
--shadow: 0 4px 16px rgba(0,0,0,.12);
--shadow-lg: 0 12px 40px rgba(0,0,0,.16);
}
/* ── 리셋 ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; font-size: 16px; }
body {
font-family: var(--font-sans);
color: var(--gray-800);
background: var(--white);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }
button { cursor: pointer; border: none; background: none; font-family: inherit; }
/* ── 공통 레이아웃 ─────────────────────────────────── */
.container {
max-width: var(--container);
margin: 0 auto;
padding: 0 24px;
}
.section {
padding: 80px 0;
}
.section-sm { padding: 48px 0; }
.section-lg { padding: 120px 0; }
.section-header {
text-align: center;
margin-bottom: 56px;
}
.section-label {
display: inline-block;
font-size: 13px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 12px;
}
.section-title {
font-size: clamp(28px, 4vw, 44px);
font-weight: 900;
color: var(--gray-900);
line-height: 1.2;
}
.section-title em {
color: var(--primary);
font-style: normal;
}
.section-desc {
margin-top: 16px;
font-size: 17px;
color: var(--gray-600);
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
/* ── 버튼 ──────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
border-radius: var(--radius);
font-size: 15px;
font-weight: 600;
transition: all var(--mid) var(--ease);
line-height: 1;
}
.btn-primary {
background: var(--primary);
color: var(--white);
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,81,162,.3);
}
.btn-outline {
border: 2px solid var(--primary);
color: var(--primary);
}
.btn-outline:hover {
background: var(--primary);
color: var(--white);
}
.btn-white {
background: var(--white);
color: var(--primary);
font-weight: 700;
}
.btn-white:hover {
background: var(--gray-100);
transform: translateY(-2px);
}
.btn-lg { padding: 16px 36px; font-size: 16px; }
.btn-sm { padding: 8px 20px; font-size: 13px; }
/* ── 카드 ──────────────────────────────────────────── */
.card {
background: var(--white);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
border: 1px solid var(--gray-200);
transition: all var(--mid) var(--ease);
overflow: hidden;
}
.card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-4px);
border-color: var(--primary-light);
}
/* ── 그리드 ────────────────────────────────────────── */
.grid-2 { display: grid; grid-template-columns: repeat(2,1fr); gap: 24px; }
.grid-3 { display: grid; grid-template-columns: repeat(3,1fr); gap: 24px; }
.grid-4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 24px; }
/* ── 배지 ──────────────────────────────────────────── */
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-primary { background: var(--primary-light); color: var(--primary); }
.badge-accent { background: rgba(0,163,224,.12); color: var(--accent-dark); }
.badge-new { background: var(--danger); color: var(--white); }
/* ── 구분선 ────────────────────────────────────────── */
.divider {
width: 48px; height: 4px;
background: var(--accent);
border-radius: 2px;
margin: 16px auto 0;
}
.divider-left { margin-left: 0; }
/* ── 스크롤바 ──────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--gray-100); }
::-webkit-scrollbar-thumb { background: var(--gray-400); border-radius: 3px; }
/* ── 페이드인 애니메이션 ────────────────────────────── */
@keyframes fadeUp {
from { opacity:0; transform:translateY(30px); }
to { opacity:1; transform:translateY(0); }
}
.fade-up { animation: fadeUp var(--slow) var(--ease) both; }
/* ── 반응형 ────────────────────────────────────────── */
@media (max-width: 1024px) {
.grid-4 { grid-template-columns: repeat(2,1fr); }
}
@media (max-width: 768px) {
.section { padding: 60px 0; }
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
.container { padding: 0 16px; }
}

View File

@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
}
}
},
build: {
outDir: '../backend/src/main/resources/static',
emptyOutDir: true,
}
});