diff --git a/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java b/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java index 001d2ae..d0f5fa5 100644 --- a/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java +++ b/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java @@ -16,6 +16,7 @@ public class DataInitializer implements CommandLineRunner { private final AdminUserRepository adminUserRepo; private final RecruitRepository recruitRepo; private final CompanyHistoryRepository historyRepo; + private final FaqRepository faqRepo; private final PasswordEncoder passwordEncoder; @Override @@ -24,6 +25,7 @@ public class DataInitializer implements CommandLineRunner { initNews(); initRecruits(); initHistory(); + initFaq(); } private void initAdmin() { @@ -147,4 +149,39 @@ public class DataInitializer implements CommandLineRunner { .build()); } } + + private void initFaq() { + if (faqRepo.count() > 0) return; + + Object[][] faqs = { + {"GUARDiA ITSM", "GUARDiA ITSM은 어떤 제품인가요?", + "GUARDiA ITSM은 메신저 한 줄 명령으로 1,000개 이상 공공기관의 레거시 IT 인프라를 자동 운영하는 AI 기반 ChatOps 플랫폼입니다. 대상 서버에 별도 소프트웨어 설치 없이 표준 SSH/SFTP 프로토콜만으로 배포·운영·모니터링을 자동화합니다.", 1}, + {"GUARDiA ITSM", "서버에 에이전트를 설치해야 하나요?", + "아니요. GUARDiA ITSM은 에이전트리스(Agentless) 방식으로 동작합니다. 대상 서버에 어떠한 소프트웨어도 설치할 필요가 없으며, 표준 SSH(22번 포트)만 열려 있으면 즉시 연동 가능합니다.", 2}, + {"GUARDiA ITSM", "클라우드 없이 사용할 수 있나요?", + "예. GUARDiA ITSM은 완전한 온프레미스(On-premise) 솔루션으로, 외부 클라우드나 인터넷 연결 없이 폐쇄망 환경에서도 100% 동작합니다. AI 엔진(Ollama)도 내부 서버에서 구동됩니다.", 3}, + {"GUARDiA ITSM", "지원되는 운영체제는 무엇인가요?", + "GUARDiA ITSM 서버: Ubuntu 20.04+, CentOS 7+, RHEL 8+, Windows Server 2019+를 지원합니다. 관리 대상 서버: SSH가 지원되는 모든 Linux/Unix/Windows Server 환경에서 사용 가능합니다.", 4}, + {"도입·계약", "도입 비용은 어떻게 되나요?", + "기관 규모와 관리 서버 수에 따라 맞춤 견적을 제공합니다. 7일 무료 체험판을 먼저 신청하신 후 문의 주시면 상세한 견적을 안내해 드립니다.", 1}, + {"도입·계약", "체험판을 사용할 수 있나요?", + "예. 7일 무료 체험판을 제공합니다. 문의하기 또는 GUARDiA 페이지의 '무료 데모 신청' 버튼을 통해 신청하시면 영업일 기준 1일 이내에 안내 드립니다.", 2}, + {"도입·계약", "공공기관 나라장터 조달 구매가 가능한가요?", + "예. GUARDiA ITSM은 조달청 나라장터 등록을 준비 중이며, 공공기관 입찰을 통한 구매를 지원합니다. 자세한 사항은 영업팀(031-483-1766)에 문의해 주십시오.", 3}, + {"기술 지원", "기술 지원은 어떻게 받을 수 있나요?", + "이메일(support@zioinfo.co.kr), 전화(031-483-1766), GUARDiA ITSM 내 챗봇을 통해 기술 지원을 제공합니다. 운영 중 긴급 장애는 24시간 온콜 지원이 가능합니다.", 1}, + {"기술 지원", "업그레이드는 어떻게 진행되나요?", + "정기 업데이트는 연 2~4회 제공되며, 보안 패치는 즉시 제공됩니다. 업그레이드는 GUARDiA 내 자동 배포 기능을 통해 다운타임 없이 진행할 수 있습니다.", 2}, + }; + + for (Object[] f : faqs) { + faqRepo.save(Faq.builder() + .category((String) f[0]) + .question((String) f[1]) + .answer((String) f[2]) + .orderNum((Integer) f[3]) + .visible(true) + .build()); + } + } } diff --git a/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java b/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java index ee4026b..58192b5 100644 --- a/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java +++ b/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java @@ -22,6 +22,7 @@ public class AdminController { private final RecruitRepository recruitRepo; private final MemberRepository memberRepo; private final CompanyHistoryRepository historyRepo; + private final FaqRepository faqRepo; private final JwtUtil jwtUtil; private final PasswordEncoder passwordEncoder; @@ -147,6 +148,38 @@ public class AdminController { }).orElse(ResponseEntity.notFound().build()); } + // ── FAQ 관리 ───────────────────────────────────────────── + @GetMapping("/faq") + public ResponseEntity adminFaqList() { + return ResponseEntity.ok(faqRepo.findAllByOrderByCategoryAscOrderNumAsc()); + } + + @PostMapping("/faq") + public ResponseEntity createFaq(@RequestBody kr.co.zioinfo.web.model.Faq faq) { + faq.setId(null); faq.setCreatedAt(null); + return ResponseEntity.ok(faqRepo.save(faq)); + } + + @PutMapping("/faq/{id}") + public ResponseEntity updateFaq(@PathVariable Long id, + @RequestBody kr.co.zioinfo.web.model.Faq body) { + return faqRepo.findById(id).map(f -> { + f.setCategory(body.getCategory()); + f.setQuestion(body.getQuestion()); + f.setAnswer(body.getAnswer()); + f.setOrderNum(body.getOrderNum()); + f.setVisible(body.isVisible()); + return ResponseEntity.ok(faqRepo.save(f)); + }).orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/faq/{id}") + public ResponseEntity deleteFaq(@PathVariable Long id) { + if (!faqRepo.existsById(id)) return ResponseEntity.notFound().build(); + faqRepo.deleteById(id); + return ResponseEntity.noContent().build(); + } + // ── 문의 관리 ──────────────────────────────────────────── @GetMapping("/inquiries") public ResponseEntity> adminInquiries( diff --git a/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java b/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java index 29b906d..17a17e7 100644 --- a/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java +++ b/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java @@ -1,9 +1,11 @@ package kr.co.zioinfo.web.controller; import kr.co.zioinfo.web.model.CompanyHistory; +import kr.co.zioinfo.web.model.Faq; import kr.co.zioinfo.web.model.Inquiry; import kr.co.zioinfo.web.model.News; import kr.co.zioinfo.web.repository.CompanyHistoryRepository; +import kr.co.zioinfo.web.repository.FaqRepository; import kr.co.zioinfo.web.repository.RecruitRepository; import kr.co.zioinfo.web.service.InquiryService; import kr.co.zioinfo.web.service.NewsService; @@ -23,6 +25,7 @@ public class ApiController { private final InquiryService inquiryService; private final RecruitRepository recruitRepo; private final CompanyHistoryRepository historyRepo; + private final FaqRepository faqRepo; // ── 회사 정보 ──────────────────────────────────────────────── @GetMapping("/company") @@ -138,6 +141,21 @@ public class ApiController { return ResponseEntity.ok(recruitRepo.findByActiveTrueOrderByCreatedAtDesc()); } + // ── FAQ (공개) ──────────────────────────────────────────────── + @GetMapping("/faq") + public ResponseEntity getFaq() { + var items = faqRepo.findByVisibleTrueOrderByOrderNumAscCategoryAscCreatedAtAsc(); + // category별 그룹핑 + var grouped = new java.util.LinkedHashMap>>(); + for (var f : items) { + grouped.computeIfAbsent(f.getCategory(), k -> new java.util.ArrayList<>()) + .add(Map.of("id", f.getId(), "q", f.getQuestion(), "a", f.getAnswer())); + } + var result = new java.util.ArrayList<>(); + grouped.forEach((cat, list) -> result.add(Map.of("cat", cat, "items", list))); + return ResponseEntity.ok(result); + } + // ── 메뉴 구조 ──────────────────────────────────────────────── @GetMapping("/menu") public ResponseEntity>> getMenu() { diff --git a/backend/src/main/java/kr/co/zioinfo/web/model/Faq.java b/backend/src/main/java/kr/co/zioinfo/web/model/Faq.java new file mode 100644 index 0000000..f9134d5 --- /dev/null +++ b/backend/src/main/java/kr/co/zioinfo/web/model/Faq.java @@ -0,0 +1,31 @@ +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 = "faq") +@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class Faq { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String category; // GUARDiA ITSM / 도입·계약 / 기술 지원 + + @Column(nullable = false, length = 300) + private String question; + + @Column(nullable = false, columnDefinition = "TEXT") + private String answer; + + private int orderNum = 0; + private boolean visible = true; + + @CreatedDate + private LocalDateTime createdAt; +} diff --git a/backend/src/main/java/kr/co/zioinfo/web/repository/FaqRepository.java b/backend/src/main/java/kr/co/zioinfo/web/repository/FaqRepository.java new file mode 100644 index 0000000..1c4eac0 --- /dev/null +++ b/backend/src/main/java/kr/co/zioinfo/web/repository/FaqRepository.java @@ -0,0 +1,10 @@ +package kr.co.zioinfo.web.repository; + +import kr.co.zioinfo.web.model.Faq; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface FaqRepository extends JpaRepository { + List findByVisibleTrueOrderByOrderNumAscCategoryAscCreatedAtAsc(); + List findAllByOrderByCategoryAscOrderNumAsc(); +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b13e336..61f9afb 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,6 +30,7 @@ const AdminRecruit = lazy(() => import('./pages/admin/AdminRecruit')); const AdminSettings = lazy(() => import('./pages/admin/AdminSettings')); const AdminMember = lazy(() => import('./pages/admin/AdminMember')); const AdminHistory = lazy(() => import('./pages/admin/AdminHistory')); +const AdminFAQ = lazy(() => import('./pages/admin/AdminFAQ')); function Loading() { return ( @@ -66,6 +67,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/NewsPage.jsx b/frontend/src/pages/NewsPage.jsx index f89f3f7..8cba982 100644 --- a/frontend/src/pages/NewsPage.jsx +++ b/frontend/src/pages/NewsPage.jsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Routes, Route, NavLink, Link } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { Routes, Route, NavLink } from 'react-router-dom'; import './Common.css'; import './NewsPage.css'; @@ -31,55 +31,37 @@ function SubNav({ title }) { ); } -/* ── 뉴스룸 ── */ -const NEWS = [ - { - id:1, cat:'제품 출시', date:'2026.05.15', - title:'GUARDiA ITSM v2.0 정식 출시 — AI ChatOps 오케스트레이션 플랫폼', - summary:'메신저 한 줄 명령으로 1,000개+ 공공기관 레거시 인프라를 자동 운영하는 GUARDiA ITSM v2.0이 정식 출시되었습니다. 신규 기능으로 AI 자연어 명령, 에이전트리스 배포 엔진, 멀티테넌트 지원이 추가됐습니다.', - content: `GUARDiA ITSM v2.0은 공공기관의 레거시 IT 인프라 운영 자동화를 위한 AI 기반 플랫폼입니다.\n\n주요 신기능:\n- AI ChatOps: 메신저 자연어 명령 → Ollama LLM 파싱 → 자동 실행\n- 에이전트리스 배포: SSH/SFTP만으로 WAS 배포·롤백 자동화\n- 멀티테넌트: 1,000개+ 기관 동시 관리\n- GS인증 1등급 신청 완료\n\n자세한 사항은 GUARDiA 소개 페이지를 참조해 주십시오.`, - hot: true, - }, - { - id:2, cat:'수주 소식', date:'2026.04.20', - title:'삼성전자 차세대 CRM 시스템 DB 마이그레이션 프로젝트 수주', - summary:'(주)지오정보기술이 삼성전자 차세대 CRM 구축 프로젝트의 DB Migration/DA/튜닝을 담당합니다. EDB PostgreSQL 환경으로의 전환을 포함한 대규모 DB 현대화 작업을 수행합니다.', - content: '삼성전자와의 두 번째 협력 프로젝트로, DB 마이그레이션 및 성능 튜닝을 담당합니다.', - hot: false, - }, - { - id:3, cat:'기술 인증', date:'2026.03.10', - title:'GUARDiA ITSM GS인증 1등급 신청 완료 — TTA 심사 예정', - summary:'GUARDiA ITSM이 한국정보통신기술협회(TTA)에 GS인증 1등급을 신청하였습니다. 기능적합성, 신뢰성, 사용성, 보안성 등 ISO/IEC 25010 기준 8대 품질 특성 심사를 앞두고 있습니다.', - content: 'GS인증 심사는 2026년 9월 예정이며, 1등급 취득 시 조달청 나라장터 우선 등재가 가능합니다.', - hot: false, - }, - { - id:4, cat:'수주 소식', date:'2026.02.15', - title:'국민연금공단 차세대 시스템 구축 — AA 역할 수행', - summary:'국민연금공단 차세대 시스템 구축 프로젝트에 Application Architect(AA)로 참여합니다. JSP/Java, Nexacro, Spring 기반의 대규모 공공기관 시스템 구축을 담당합니다.', - content: '국민연금관리공단의 차세대 시스템은 수천만 가입자의 연금 관리 시스템으로, CI/CD 파이프라인 기반의 현대적인 개발 환경을 구축합니다.', - hot: false, - }, - { - id:5, cat:'기업 소식', date:'2025.12.01', - title:'2025년 사업실적 — 연간 프로젝트 10건 성공 수행', - summary:'2025년 한 해 동안 삼성전자, 서울신용보증재단, 헌법재판소 등 10개 주요 프로젝트를 성공적으로 완료했습니다. 매출은 전년 대비 25% 성장하였습니다.', - content: '창립 이래 최대 성과를 기록한 2025년 사업실적을 공유드립니다.', - hot: false, - }, - { - id:6, cat:'파트너십', date:'2025.09.10', - title:'URP 공식 파트너사 등록 — 공공기관 SI/SM 프로젝트 솔루션 강화', - summary:'인프라 사업 1위 업체의 URP 공식 파트너사로 등록되었습니다. SI/SM프로젝트는 물론, 공공기관 시스템 현대화 사업을 공동으로 추진합니다.', - content: '공공기관의 시스템 유지보수 및 구축 비용 절감을 위한 AI 전환 프로젝트를 전문적으로 지원합니다.', - hot: false, - }, -]; +function useNews(category) { + const [news, setNews] = useState([]); + const [loading, setLoading] = useState(true); + useEffect(() => { + fetch(`/api/news?category=${encodeURIComponent(category)}&size=20`) + .then(r => r.json()) + .then(data => setNews(data.content ?? data)) + .catch(() => setNews([])) + .finally(() => setLoading(false)); + }, [category]); + return { news, loading }; +} +function formatDate(dt) { + if (!dt) return ''; + return String(dt).slice(0, 10).replace(/-/g, '.'); +} + +/* ── 뉴스룸 ── */ function Newsroom() { + const { news, loading } = useNews('뉴스룸'); const [selected, setSelected] = useState(null); - const item = NEWS.find(n => n.id === selected); + const item = news.find(n => n.id === selected); + + if (loading) return ( +
+ +
불러오는 중...
+
+ ); + return (
@@ -89,33 +71,38 @@ function Newsroom() {
- {item.cat} + {item.category}

{item.title}

-

{item.date}

+

{formatDate(item.createdAt)}

- {item.content.split('\n').map((p, i) => ( + {(item.content || item.summary || '').split('\n').map((p, i) => ( p.trim() ?

{p}

: null ))}
+ ) : news.length === 0 ? ( +
+

📰

+

등록된 뉴스가 없습니다.

+

관리자 페이지에서 뉴스를 추가해 주세요.

+
) : ( <> - {/* 메인 뉴스 */} -
setSelected(NEWS[0].id)}> +
setSelected(news[0].id)} style={{ cursor: 'pointer' }}>
- 🔥 {NEWS[0].cat} -

{NEWS[0].title}

-

{NEWS[0].summary}

- {NEWS[0].date} + 🔥 {news[0].category} +

{news[0].title}

+

{news[0].summary}

+ {formatDate(news[0].createdAt)}
- {NEWS.slice(1).map(n => ( -
setSelected(n.id)}> - {n.cat} + {news.slice(1).map(n => ( +
setSelected(n.id)} style={{ cursor: 'pointer' }}> + {n.category}

{n.title}

{n.summary}

- {n.date} + {formatDate(n.createdAt)}
))}
@@ -128,79 +115,68 @@ function Newsroom() { } /* ── 기술 블로그 ── */ -const BLOGS = [ - { - id:1, tag:'AI·LLM', date:'2026.05.20', - title:'온프레미스 Ollama로 폐쇄망 ChatOps 구현하기', - summary:'인터넷 없이 내부망에서 LLM을 운영하는 방법. Llama-3-8B 모델을 Ollama로 구동하고 FastAPI와 연동하는 전체 과정을 설명합니다.', - readMin: 12, - }, - { - id:2, tag:'DevOps', date:'2026.05.10', - title:'에이전트리스 WAS 배포 자동화 — paramiko SSH로 레거시 서버 관리', - summary:'JEUS·Tomcat 등 레거시 WAS에 SSH/SFTP만으로 배포하는 방법. 백업→배포→헬스체크→롤백 파이프라인 구현 예제.', - readMin: 15, - }, - { - id:3, tag:'보안', date:'2026.04.28', - title:'AES-256-GCM으로 서버 자격증명을 안전하게 저장하는 법', - summary:'공공기관 서버 SSH 비밀번호를 DB에 안전하게 암호화 저장하는 방법. IV·암호문·GCM Tag 구조 설계와 Python 구현.', - readMin: 8, - }, - { - id:4, tag:'데이터베이스', date:'2026.04.15', - title:'Oracle 19c → EDB PostgreSQL 마이그레이션 실전 가이드', - summary:'삼성전자 CRM 프로젝트에서 실제 수행한 Oracle→EDB 마이그레이션 경험 공유. Smeta, ExemOne 활용 SQL 변환 전략.', - readMin: 20, - }, - { - id:5, tag:'성능', date:'2026.03.25', - title:'공공기관 행정정보시스템 SQL 튜닝 — 서울시립대 사례', - summary:'대학행정정보시스템 성능 개선 프로젝트 실전 사례. JMeter 부하테스트와 Oracle 실행계획 분석으로 응답시간 60% 단축.', - readMin: 18, - }, - { - id:6, tag:'아키텍처', date:'2026.03.10', - title:'FastAPI 비동기 WebSocket으로 실시간 대시보드 구축하기', - summary:'GUARDiA ITSM 실시간 모니터링 대시보드 구현 방법. FastAPI SSE + WebSocket + React를 조합한 풀스택 아키텍처.', - readMin: 14, - }, -]; - -const TAG_COLORS = { - 'AI·LLM': '#7c3aed', 'DevOps': '#0051A2', '보안': '#dc2626', - '데이터베이스': '#d97706', '성능': '#059669', '아키텍처': '#0891b2' -}; - function Blog() { + const { news: blogs, loading } = useNews('기술블로그'); + const [selected, setSelected] = useState(null); + const item = blogs.find(b => b.id === selected); + + if (loading) return ( +
+ +
불러오는 중...
+
+ ); + return (
-
- Tech Blog -

기술 인사이트 공유

-

20년 이상의 프로젝트 경험에서 얻은 기술 노하우를 공유합니다

-
-
- {BLOGS.map(b => ( -
-
- {b.tag} -
-

{b.title}

-

{b.summary}

-
- 📅 {b.date} - ⏱ {b.readMin}분 읽기 -
- + {item ? ( +
+ +
+ {item.category} +

{item.title}

+

{formatDate(item.createdAt)}

+
+ {(item.content || item.summary || '').split('\n').map((p, i) => ( + p.trim() ?

{p}

: null + ))}
- ))} -
+
+ ) : blogs.length === 0 ? ( +
+

✍️

+

등록된 블로그 포스트가 없습니다.

+

관리자 페이지에서 블로그를 추가해 주세요.

+
+ ) : ( + <> +
+ Tech Blog +

기술 인사이트 공유

+

20년 이상의 프로젝트 경험에서 얻은 기술 노하우를 공유합니다

+
+
+ {blogs.map(b => ( +
setSelected(b.id)} style={{ cursor: 'pointer' }}> +
+ {b.category} +
+

{b.title}

+

{b.summary}

+
+ 📅 {formatDate(b.createdAt)} +
+ +
+ ))} +
+ + )}
diff --git a/frontend/src/pages/Support.jsx b/frontend/src/pages/Support.jsx index e96e354..422778f 100644 --- a/frontend/src/pages/Support.jsx +++ b/frontend/src/pages/Support.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Routes, Route, NavLink } from 'react-router-dom'; import './Common.css'; import './Support.css'; @@ -34,55 +34,64 @@ function SubNav({ title }) { } /* ── 공지사항 ── */ -const NOTICES = [ - { id:1, cat:'공지', title:'GUARDiA ITSM v2.0 정식 출시 안내', date:'2026.05.15', hot:true }, - { id:2, cat:'공지', title:'2026년 상반기 유지보수 점검 일정 안내 (6월 1일~2일)', date:'2026.05.10', hot:false }, - { id:3, cat:'보안', title:'Apache Log4j 취약점 긴급 패치 안내', date:'2026.04.28', hot:false }, - { id:4, cat:'공지', title:'개인정보처리방침 개정 안내 (2026년 4월)', date:'2026.04.01', hot:false }, - { id:5, cat:'이벤트', title:'2026 공공기관 디지털전환 세미나 참가 안내 (5월 20일)', date:'2026.03.25', hot:false }, - { id:6, cat:'공지', title:'GUARDiA ITSM GS인증 1등급 신청 완료 안내', date:'2026.03.10', hot:false }, - { id:7, cat:'공지', title:'신규 파트너사 협약 체결 — URP 공식 파트너 등록', date:'2026.02.20', hot:false }, - { id:8, cat:'보안', title:'2026년 정보보안 교육 실시 안내 (임직원 필독)', date:'2026.01.15', hot:false }, - { id:9, cat:'공지', title:'2025년 사업성과 및 2026년 사업계획 발표', date:'2026.01.02', hot:false }, - { id:10,'cat':'공지', title:'연말연시 고객지원팀 운영시간 안내 (12/24~1/3)', date:'2025.12.20', hot:false }, -]; -const CAT_COLORS = { '공지':'var(--primary)', '보안':'var(--danger)', '이벤트':'var(--accent)' }; +const CAT_COLORS = { '공지':'var(--primary)', '보안':'#dc2626', '이벤트':'var(--accent)', '공지사항':'var(--primary)' }; + +function formatDate(dt) { + if (!dt) return ''; + return String(dt).slice(0, 10).replace(/-/g, '.'); +} function Notice() { + const [notices, setNotices] = useState([]); + const [loading, setLoading] = useState(true); const [selected, setSelected] = useState(null); + + useEffect(() => { + fetch('/api/news?category=공지사항&size=30') + .then(r => r.json()) + .then(d => setNotices(d.content ?? d)) + .catch(() => setNotices([])) + .finally(() => setLoading(false)); + }, []); + + const item = notices.find(n => n.id === selected); + return (
- {selected ? ( + {loading ? ( +
불러오는 중...
+ ) : item ? (
- {selected.cat} -

{selected.title}

-

{selected.date}

+ {item.category} +

{item.title}

+

{formatDate(item.createdAt)}

-

안녕하세요, (주)지오정보기술입니다.

-

본 공지는 {selected.title}에 관한 안내입니다.

-

자세한 사항은 고객지원팀(031-483-1766)으로 문의해 주시기 바랍니다.

-

감사합니다.

+ {item.content + ? item.content.split('\n').map((p, i) => p.trim() ?

{p}

: null) + : <>

안녕하세요, (주)지오정보기술입니다.

자세한 사항은 고객지원팀(031-483-1766)으로 문의해 주시기 바랍니다.

}
+ ) : notices.length === 0 ? ( +
+

📋

+

등록된 공지사항이 없습니다.

+
) : (
구분제목등록일
- {NOTICES.map(n => ( -
setSelected(n)}> - {n.cat} - - {n.hot && HOT} - {n.title} - - {n.date} + {notices.map(n => ( +
setSelected(n.id)}> + {n.category} + {n.title} + {formatDate(n.createdAt)}
))}
@@ -94,36 +103,20 @@ function Notice() { } /* ── FAQ ── */ -const FAQS = [ - { - cat: 'GUARDiA ITSM', - items: [ - { q: 'GUARDiA ITSM은 어떤 제품인가요?', a: 'GUARDiA ITSM은 메신저 한 줄 명령으로 1,000개 이상 공공기관의 레거시 IT 인프라를 자동 운영하는 AI 기반 ChatOps 플랫폼입니다. 대상 서버에 별도 소프트웨어 설치 없이 표준 SSH/SFTP 프로토콜만으로 배포·운영·모니터링을 자동화합니다.' }, - { q: '서버에 에이전트를 설치해야 하나요?', a: '아니요. GUARDiA ITSM은 에이전트리스(Agentless) 방식으로 동작합니다. 대상 서버에 어떠한 소프트웨어도 설치할 필요가 없으며, 표준 SSH(22번 포트)만 열려 있으면 즉시 연동 가능합니다.' }, - { q: '클라우드 없이 사용할 수 있나요?', a: '예. GUARDiA ITSM은 완전한 온프레미스(On-premise) 솔루션으로, 외부 클라우드나 인터넷 연결 없이 폐쇄망 환경에서도 100% 동작합니다. AI 엔진(Ollama)도 내부 서버에서 구동됩니다.' }, - { q: '지원되는 운영체제는 무엇인가요?', a: 'GUARDiA ITSM 서버: Ubuntu 20.04+, CentOS 7+, RHEL 8+, Windows Server 2019+를 지원합니다. 관리 대상 서버: SSH가 지원되는 모든 Linux/Unix/Windows Server 환경에서 사용 가능합니다.' }, - ] - }, - { - cat: '도입·계약', - items: [ - { q: '도입 비용은 어떻게 되나요?', a: '기관 규모와 관리 서버 수에 따라 맞춤 견적을 제공합니다. 7일 무료 체험판을 먼저 신청하신 후 문의 주시면 상세한 견적을 안내해 드립니다.' }, - { q: '체험판을 사용할 수 있나요?', a: '예. 7일 무료 체험판을 제공합니다. 문의하기 또는 GUARDiA 페이지의 "무료 데모 신청" 버튼을 통해 신청하시면 영업일 기준 1일 이내에 안내 드립니다.' }, - { q: '공공기관 나라장터 조달 구매가 가능한가요?', a: '예. GUARDiA ITSM은 조달청 나라장터 등록을 준비 중이며, 공공기관 입찰을 통한 구매를 지원합니다. 자세한 사항은 영업팀(031-483-1766)에 문의해 주십시오.' }, - ] - }, - { - cat: '기술 지원', - items: [ - { q: '기술 지원은 어떻게 받을 수 있나요?', a: '이메일(support@zioinfo.co.kr), 전화(031-483-1766), GUARDiA ITSM 내 챗봇을 통해 기술 지원을 제공합니다. 운영 중 긴급 장애는 24시간 온콜 지원이 가능합니다.' }, - { q: '업그레이드는 어떻게 진행되나요?', a: '정기 업데이트는 연 2~4회 제공되며, 보안 패치는 즉시 제공됩니다. 업그레이드는 GUARDiA 내 자동 배포 기능을 통해 다운타임 없이 진행할 수 있습니다.' }, - ] - }, -]; - function FAQ() { + const [faqs, setFaqs] = useState([]); + const [loading, setLoading] = useState(true); const [openIdx, setOpenIdx] = useState({}); const toggle = (ci, qi) => setOpenIdx(p => ({ ...p, [`${ci}-${qi}`]: !p[`${ci}-${qi}`] })); + + useEffect(() => { + fetch('/api/faq') + .then(r => r.json()) + .then(data => setFaqs(data)) + .catch(() => setFaqs([])) + .finally(() => setLoading(false)); + }, []); + return (
@@ -133,7 +126,13 @@ function FAQ() { FAQ

자주 묻는 질문

- {FAQS.map((cat, ci) => ( + {loading ? ( +
불러오는 중...
+ ) : faqs.length === 0 ? ( +
+

등록된 FAQ가 없습니다.

+
+ ) : faqs.map((cat, ci) => (

{cat.cat}

{cat.items.map((item, qi) => { diff --git a/frontend/src/pages/admin/AdminFAQ.jsx b/frontend/src/pages/admin/AdminFAQ.jsx new file mode 100644 index 0000000..a371627 --- /dev/null +++ b/frontend/src/pages/admin/AdminFAQ.jsx @@ -0,0 +1,136 @@ +import { useEffect, useState, useCallback } from 'react'; + +const token = () => localStorage.getItem('admin_token'); +const authFetch = (url, opts = {}) => + fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}`, ...opts.headers } }); + +const EMPTY = { category: 'GUARDiA ITSM', question: '', answer: '', orderNum: 0, visible: true }; +const CATS = ['GUARDiA ITSM', '도입·계약', '기술 지원', '일반']; + +export default function AdminFAQ() { + const [list, setList] = useState([]); + const [modal, setModal] = useState(null); // null | 'add' | 'edit' + const [form, setForm] = useState(EMPTY); + const [editId, setEditId] = useState(null); + const [filterCat, setFilterCat] = useState(''); + + const load = useCallback(() => { + authFetch('/api/admin/faq').then(r => r.json()).then(setList).catch(() => {}); + }, []); + + useEffect(() => { load(); }, [load]); + + const open = (item = null) => { + if (item) { setForm({ ...item }); setEditId(item.id); setModal('edit'); } + else { setForm(EMPTY); setEditId(null); setModal('add'); } + }; + + const save = async () => { + const url = modal === 'edit' ? `/api/admin/faq/${editId}` : '/api/admin/faq'; + const method = modal === 'edit' ? 'PUT' : 'POST'; + await authFetch(url, { method, body: JSON.stringify(form) }); + setModal(null); load(); + }; + + const del = async (id) => { + if (!window.confirm('삭제하시겠습니까?')) return; + await authFetch(`/api/admin/faq/${id}`, { method: 'DELETE' }); + load(); + }; + + const filtered = filterCat ? list.filter(f => f.category === filterCat) : list; + + return ( +
+
+

FAQ 관리

+ +
+ + {/* 카테고리 필터 */} +
+ {['전체', ...CATS].map(c => ( + + ))} +
+ + {/* FAQ 목록 */} +
+ {filtered.length === 0 ? ( +
등록된 FAQ가 없습니다.
+ ) : filtered.map(f => ( +
+
+
+ {f.category} +

Q. {f.question}

+

A. {f.answer}

+
+
+ {f.visible ? '공개' : '비공개'} + + +
+
+
+ ))} +
+ + {/* 모달 */} + {modal && ( +
+
+

{modal === 'edit' ? 'FAQ 수정' : 'FAQ 추가'}

+ + + + + +