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>
101
backend/pom.xml
Normal 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>
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
40
backend/src/main/java/kr/co/zioinfo/web/model/Inquiry.java
Normal 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;
|
||||||
|
}
|
||||||
36
backend/src/main/java/kr/co/zioinfo/web/model/News.java
Normal 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;
|
||||||
|
}
|
||||||
@ -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> {}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
49
backend/src/main/resources/application.yml
Normal 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
|
||||||
26
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
18
frontend/public/index.html
Normal 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>
|
||||||
BIN
frontend/public/logo-white.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/logo.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
frontend/public/screenshots/01_dashboard.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
frontend/public/screenshots/02_sr_list.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
frontend/public/screenshots/03_si_project.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/public/screenshots/04_incidents.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
frontend/public/screenshots/05_agents.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
frontend/public/screenshots/06_license.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
42
frontend/src/App.jsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
frontend/src/components/layout/Footer.css
Normal 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; }
|
||||||
|
}
|
||||||
111
frontend/src/components/layout/Footer.jsx
Normal 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 © 2026 (주)지오정보기술 All Rights Reserved.
|
||||||
|
</p>
|
||||||
|
<div className="footer-powered">
|
||||||
|
Powered by <strong>GUARDiA ITSM</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
frontend/src/components/layout/Header.css
Normal 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; }
|
||||||
|
}
|
||||||
159
frontend/src/components/layout/Header.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
frontend/src/main.jsx
Normal 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>
|
||||||
|
);
|
||||||
7
frontend/src/pages/Common.css
Normal 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; }
|
||||||
38
frontend/src/pages/Company.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
frontend/src/pages/Contact.css
Normal 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; } }
|
||||||
126
frontend/src/pages/Contact.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
281
frontend/src/pages/GuardiaDetail.css
Normal 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; }
|
||||||
|
}
|
||||||
405
frontend/src/pages/GuardiaDetail.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
303
frontend/src/pages/Home.css
Normal 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; }
|
||||||
|
}
|
||||||
315
frontend/src/pages/Home.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
frontend/src/pages/NewsPage.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
frontend/src/pages/NotFound.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
frontend/src/pages/Recruit.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
215
frontend/src/styles/global.css
Normal 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; }
|
||||||
|
}
|
||||||
19
frontend/vite.config.js
Normal 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,
|
||||||
|
}
|
||||||
|
});
|
||||||