From 8d1168f50cc41edc41c4d3a792a77637fba1feb0 Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Sun, 31 May 2026 17:52:02 +0900 Subject: [PATCH] feat(history): company history DB management + admin CRUD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CompanyHistory JPA entity (tb_company_history) - CompanyHistoryRepository - GET /api/history: DB-based grouped history (year + items[]) - Admin CRUD: GET/POST/PUT/DELETE /api/admin/history - DataInitializer: 35 history items seeded from 2000 to 2026 - Company.jsx: useHistory() hook -> API fetch with fallback - AdminHistory.jsx: year-grouped timeline CRUD UI - AdminLayout: 회사 연혁 menu added Co-Authored-By: Claude Sonnet 4.6 --- .../zioinfo/web/config/DataInitializer.java | 53 +++++ .../web/controller/AdminController.java | 48 +++++ .../zioinfo/web/controller/ApiController.java | 45 ++-- .../co/zioinfo/web/model/CompanyHistory.java | 38 ++++ .../repository/CompanyHistoryRepository.java | 21 ++ workspace/zioinfo-web/frontend/src/App.jsx | 2 + .../frontend/src/pages/Company.jsx | 132 ++++-------- .../frontend/src/pages/admin/AdminHistory.jsx | 203 ++++++++++++++++++ .../frontend/src/pages/admin/AdminLayout.jsx | 1 + 9 files changed, 428 insertions(+), 115 deletions(-) create mode 100644 workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/model/CompanyHistory.java create mode 100644 workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/repository/CompanyHistoryRepository.java create mode 100644 workspace/zioinfo-web/frontend/src/pages/admin/AdminHistory.jsx diff --git a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java index bf8114ac..001d2ae0 100644 --- a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java +++ b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/config/DataInitializer.java @@ -15,6 +15,7 @@ public class DataInitializer implements CommandLineRunner { private final NewsRepository newsRepo; private final AdminUserRepository adminUserRepo; private final RecruitRepository recruitRepo; + private final CompanyHistoryRepository historyRepo; private final PasswordEncoder passwordEncoder; @Override @@ -22,6 +23,7 @@ public class DataInitializer implements CommandLineRunner { initAdmin(); initNews(); initRecruits(); + initHistory(); } private void initAdmin() { @@ -94,4 +96,55 @@ public class DataInitializer implements CommandLineRunner { .preferred("- 공공기관 정보보호 인증 자격증 (정보처리기사 등)\n- Kubernetes 경험") .deadline(LocalDate.of(2026, 7, 31)).headcount(1).active(true).build()); } + + private void initHistory() { + if (historyRepo.count() > 0) return; + + Object[][] data = { + {"2026", 0, "GUARDiA ITSM v2.0 출시 — AI ChatOps 오케스트레이션 플랫폼"}, + {"2026", 1, "GS인증 1등급 신청 준비 완료 (TTA 심사 예정)"}, + {"2026", 2, "공공기관 1,000개 이상 멀티테넌트 지원 목표 달성"}, + {"2025", 0, "삼성전자 차세대 CRM 구축 (DB Migration / DA / 튜닝)"}, + {"2025", 1, "GUARDiA ITSM v1.0 베타 서비스 개시"}, + {"2025", 2, "AI 기반 인프라 자동화 특허 출원"}, + {"2024", 0, "DELL 차세대 CRM 구축 — DBA 역할 수행 (엠로)"}, + {"2024", 1, "소상공인컨설팅시스템 구축 (서울신용보증재단, PM)"}, + {"2024", 2, "국민연금 차세대 시스템 구축 (AA)"}, + {"2023", 0, "헌법재판소 포털시스템 구축 (PM)"}, + {"2023", 1, "서울신용보증재단 모바일앱 구축 완료 (PM)"}, + {"2022", 0, "에이텍에이피 통합유지보수관리시스템 개발 (PM)"}, + {"2022", 1, "헌법재판소 통합보안관제시스템 구축 (PM)"}, + {"2020–2021", 0, "현대백화점 HKOS 시스템 개발/구축 (PM)"}, + {"2020–2021", 1, "서울시립대 대학행정정보시스템 성능 개선 (PL)"}, + {"2020–2021", 2, "농협 하나로마트 ESL 시스템 구축 (PM)"}, + {"2018–2019", 0, "이마트 정산시스템 프로젝트 (DA)"}, + {"2018–2019", 1, "우체국금융 스마트ATM 도입 (PMO)"}, + {"2018–2019", 2, "현대백화점 무인POS시스템 구축 (PM)"}, + {"2018–2019", 3, "갤러리아백화점 PDA 정산시스템 (PM)"}, + {"2015–2017", 0, "LG U+ VAN 고도화 — 승인시스템 개발 FEP/AP/BEP (AA)"}, + {"2015–2017", 1, "한화그룹 4사 통합 HR시스템 구축 (PL)"}, + {"2015–2017", 2, "참좋은여행 콜센터 어플리케이션 구축 (PL)"}, + {"2013–2014", 0, "삼성전자 품질관리시스템(QWINGS) 구축 (PM)"}, + {"2013–2014", 1, "대우증권 통합인프라시스템 (DBA)"}, + {"2013–2014", 2, "현대캐피탈 차세대시스템 (PL)"}, + {"2013–2014", 3, "중소기업 1357 통합콜센터 구축 (PL)"}, + {"2010–2012", 0, "삼성전자서비스 eZone 갱신 (PL)"}, + {"2010–2012", 1, "현대모비스 원가관리시스템 (DBA)"}, + {"2010–2012", 2, "한국전기안전공사 전기안전포털시스템 (DBA)"}, + {"2008–2009", 0, "국민은행 차세대 포탈 구축 (PL)"}, + {"2008–2009", 1, "한국원자력연료 인사정보(HMS)시스템 (DBA)"}, + {"2008–2009", 2, "한국전기안전공사 안전점검 고도화 (DBA)"}, + {"2000", 0, "(주)지오정보기술 창립"}, + {"2000", 1, "공공기관 IT 인프라 서비스 개시"}, + }; + + for (Object[] row : data) { + historyRepo.save(CompanyHistory.builder() + .year((String) row[0]) + .sortOrder((Integer) row[1]) + .content((String) row[2]) + .visible(true) + .build()); + } + } } diff --git a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java index d5b0c926..ee4026b5 100644 --- a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java +++ b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/AdminController.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; import java.util.*; +import java.util.stream.Collectors; @RestController @RequestMapping("/api/admin") @@ -20,6 +21,7 @@ public class AdminController { private final InquiryRepository inquiryRepo; private final RecruitRepository recruitRepo; private final MemberRepository memberRepo; + private final CompanyHistoryRepository historyRepo; private final JwtUtil jwtUtil; private final PasswordEncoder passwordEncoder; @@ -55,6 +57,52 @@ public class AdminController { return ResponseEntity.ok(stats); } + // ── 연혁 관리 (CRUD) ───────────────────────────────────── + @GetMapping("/history") + public ResponseEntity> adminHistory() { + return ResponseEntity.ok(historyRepo.findAllByOrderByYearDescSortOrderAsc()); + } + + @PostMapping("/history") + public ResponseEntity createHistory(@RequestBody CompanyHistory h) { + h.setId(null); + return ResponseEntity.ok(historyRepo.save(h)); + } + + @PutMapping("/history/{id}") + public ResponseEntity updateHistory( + @PathVariable Long id, @RequestBody CompanyHistory body) { + return historyRepo.findById(id).map(h -> { + h.setYear(body.getYear()); + h.setContent(body.getContent()); + h.setSortOrder(body.getSortOrder()); + h.setVisible(body.isVisible()); + return ResponseEntity.ok(historyRepo.save(h)); + }).orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/history/{id}") + public ResponseEntity deleteHistory(@PathVariable Long id) { + historyRepo.deleteById(id); + return ResponseEntity.noContent().build(); + } + + /** 연도별 그룹 조회 (프론트 미리보기용) */ + @GetMapping("/history/grouped") + public ResponseEntity>> adminHistoryGrouped() { + List rows = historyRepo.findAllByOrderByYearDescSortOrderAsc(); + Map>> grouped = new LinkedHashMap<>(); + for (CompanyHistory h : rows) { + grouped.computeIfAbsent(h.getYear(), k -> new ArrayList<>()) + .add(Map.of("id", h.getId(), "content", h.getContent(), + "sortOrder", h.getSortOrder(), "visible", h.isVisible())); + } + List> result = grouped.entrySet().stream() + .map(e -> Map.of("year", (Object)e.getKey(), "items", e.getValue())) + .collect(Collectors.toList()); + return ResponseEntity.ok(result); + } + // ── 뉴스 관리 ──────────────────────────────────────────── @GetMapping("/news") public ResponseEntity> adminNews( diff --git a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java index c8a2d87c..29b906de 100644 --- a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java +++ b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/controller/ApiController.java @@ -1,7 +1,9 @@ package kr.co.zioinfo.web.controller; +import kr.co.zioinfo.web.model.CompanyHistory; import kr.co.zioinfo.web.model.Inquiry; import kr.co.zioinfo.web.model.News; +import kr.co.zioinfo.web.repository.CompanyHistoryRepository; import kr.co.zioinfo.web.repository.RecruitRepository; import kr.co.zioinfo.web.service.InquiryService; import kr.co.zioinfo.web.service.NewsService; @@ -20,6 +22,7 @@ public class ApiController { private final NewsService newsService; private final InquiryService inquiryService; private final RecruitRepository recruitRepo; + private final CompanyHistoryRepository historyRepo; // ── 회사 정보 ──────────────────────────────────────────────── @GetMapping("/company") @@ -40,33 +43,25 @@ public class ApiController { return ResponseEntity.ok(info); } - // ── 연혁 ───────────────────────────────────────────────────── + // ── 연혁 (DB 기반) ─────────────────────────────────────────── @GetMapping("/history") public ResponseEntity>> getHistory() { - List> 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); + List rows = historyRepo.findByVisibleTrueOrderByYearDescSortOrderAsc(); + + // 연도별 그룹핑 + Map> grouped = new LinkedHashMap<>(); + for (CompanyHistory h : rows) { + grouped.computeIfAbsent(h.getYear(), k -> new ArrayList<>()).add(h.getContent()); + } + + List> result = new ArrayList<>(); + for (Map.Entry> e : grouped.entrySet()) { + Map m = new LinkedHashMap<>(); + m.put("year", e.getKey()); + m.put("items", e.getValue()); + result.add(m); + } + return ResponseEntity.ok(result); } // ── GUARDiA 솔루션 정보 ─────────────────────────────────────── diff --git a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/model/CompanyHistory.java b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/model/CompanyHistory.java new file mode 100644 index 00000000..372ac495 --- /dev/null +++ b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/model/CompanyHistory.java @@ -0,0 +1,38 @@ +package kr.co.zioinfo.web.model; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; + +@Entity @Table(name = "company_history") +@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class CompanyHistory { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** 연도 또는 기간 표시 (예: "2026", "2020–2021") */ + @Column(nullable = false, length = 20) + private String year; + + /** 항목 내용 */ + @Column(nullable = false, length = 500) + private String content; + + /** 같은 연도 내 순서 */ + @Column(name = "sort_order") + private int sortOrder = 0; + + /** 노출 여부 */ + private boolean visible = true; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/repository/CompanyHistoryRepository.java b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/repository/CompanyHistoryRepository.java new file mode 100644 index 00000000..1ed7e326 --- /dev/null +++ b/workspace/zioinfo-web/backend/src/main/java/kr/co/zioinfo/web/repository/CompanyHistoryRepository.java @@ -0,0 +1,21 @@ +package kr.co.zioinfo.web.repository; + +import kr.co.zioinfo.web.model.CompanyHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import java.util.List; + +public interface CompanyHistoryRepository extends JpaRepository { + + /** 공개 연혁만, 연도 역순 → 순서 오름차순 */ + List findByVisibleTrueOrderByYearDescSortOrderAsc(); + + /** 관리자용 전체 목록 */ + List findAllByOrderByYearDescSortOrderAsc(); + + /** 연도별 항목 수 */ + long countByYear(String year); + + /** 특정 연도 삭제 */ + void deleteByYear(String year); +} diff --git a/workspace/zioinfo-web/frontend/src/App.jsx b/workspace/zioinfo-web/frontend/src/App.jsx index d4cf7087..b13e3364 100644 --- a/workspace/zioinfo-web/frontend/src/App.jsx +++ b/workspace/zioinfo-web/frontend/src/App.jsx @@ -29,6 +29,7 @@ const AdminInquiry = lazy(() => import('./pages/admin/AdminInquiry')); const AdminRecruit = lazy(() => import('./pages/admin/AdminRecruit')); const AdminSettings = lazy(() => import('./pages/admin/AdminSettings')); const AdminMember = lazy(() => import('./pages/admin/AdminMember')); +const AdminHistory = lazy(() => import('./pages/admin/AdminHistory')); function Loading() { return ( @@ -65,6 +66,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/workspace/zioinfo-web/frontend/src/pages/Company.jsx b/workspace/zioinfo-web/frontend/src/pages/Company.jsx index ed601827..7fd6898d 100644 --- a/workspace/zioinfo-web/frontend/src/pages/Company.jsx +++ b/workspace/zioinfo-web/frontend/src/pages/Company.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Routes, Route, NavLink, useNavigate } from 'react-router-dom'; import './Common.css'; import './Company.css'; @@ -97,83 +97,25 @@ function Greeting() { ); } -/* ── 연혁 ── */ -const HISTORY = [ +/* ── 연혁 (API에서 로드) ── */ +function useHistory() { + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + useEffect(() => { + fetch('/api/history') + .then(r => r.json()) + .then(data => setHistory(data)) + .catch(() => setHistory([])) + .finally(() => setLoading(false)); + }, []); + return { history, loading }; +} + +/* ── 연혁 (정적 폴백 — API 실패 시) ── */ +const HISTORY_FALLBACK = [ { year: '2026', items: [ 'GUARDiA ITSM v2.0 출시 — AI ChatOps 오케스트레이션 플랫폼', - 'GS인증 1등급 신청 준비 완료 (TTA 심사 예정)', - '공공기관 1,000개 이상 멀티테넌트 지원 목표 달성', - ] - }, - { - year: '2025', items: [ - '삼성전자 차세대 CRM 구축 (DB Migration / DA / 튜닝)', - 'GUARDiA ITSM v1.0 베타 서비스 개시', - 'AI 기반 인프라 자동화 특허 출원', - ] - }, - { - year: '2024', items: [ - 'DELL 차세대 CRM 구축 — DBA 역할 수행 (엠로)', - '소상공인컨설팅시스템 구축 (서울신용보증재단, PM)', - '국민연금 차세대 시스템 구축 (AA)', - ] - }, - { - year: '2023', items: [ - '헌법재판소 포털시스템 구축 (PM)', - '서울신용보증재단 모바일앱 구축 완료 (PM)', - ] - }, - { - year: '2022', items: [ - '에이텍에이피 통합유지보수관리시스템 개발 (PM)', - '헌법재판소 통합보안관제시스템 구축 (PM)', - ] - }, - { - year: '2020–2021', items: [ - '현대백화점 HKOS 시스템 개발/구축 (PM)', - '서울시립대 대학행정정보시스템 성능 개선 (PL)', - '농협 하나로마트 ESL 시스템 구축 (PM)', - ] - }, - { - year: '2018–2019', items: [ - '이마트 정산시스템 프로젝트 (DA)', - '우체국금융 스마트ATM 도입 (PMO)', - '현대백화점 무인POS시스템 구축 (PM)', - '갤러리아백화점 PDA 정산시스템 (PM)', - ] - }, - { - year: '2015–2017', items: [ - 'LG U+ VAN 고도화 — 승인시스템 개발 FEP/AP/BEP (AA)', - '한화그룹 4사 통합 HR시스템 구축 (PL)', - '참좋은여행 콜센터 어플리케이션 구축 (PL)', - ] - }, - { - year: '2013–2014', items: [ - '삼성전자 품질관리시스템(QWINGS) 구축 (PM)', - '대우증권 통합인프라시스템 (DBA)', - '현대캐피탈 차세대시스템 (PL)', - '중소기업 1357 통합콜센터 구축 (PL)', - ] - }, - { - year: '2010–2012', items: [ - '삼성전자서비스 eZone 갱신 (PL)', - '현대모비스 원가관리시스템 (DBA)', - '한국전기안전공사 전기안전포털시스템 (DBA)', - ] - }, - { - year: '2008–2009', items: [ - '국민은행 차세대 포탈 구축 (PL)', - '한국원자력연료 인사정보(HMS)시스템 (DBA)', - '한국전기안전공사 안전점검 고도화 (DBA)', ] }, { @@ -183,8 +125,12 @@ const HISTORY = [ ] }, ]; +const HISTORY = HISTORY_FALLBACK; function History() { + const { history, loading } = useHistory(); + const data = history.length > 0 ? history : HISTORY_FALLBACK; + return (
@@ -195,22 +141,28 @@ function History() {

20년+ 성장의 역사

2000년 창립 이래 국내 주요 기관·기업과 함께 성장해 왔습니다

-
- {HISTORY.map((h, i) => ( -
-
{h.year}
-
-
- {h.items.map((item, j) => ( -
- - {item} -
- ))} + {loading ? ( +
+ 연혁을 불러오는 중... +
+ ) : ( +
+ {data.map((h, i) => ( +
+
{h.year}
+
+
+ {h.items.map((item, j) => ( +
+ + {item} +
+ ))} +
-
- ))} -
+ ))} +
+ )}
diff --git a/workspace/zioinfo-web/frontend/src/pages/admin/AdminHistory.jsx b/workspace/zioinfo-web/frontend/src/pages/admin/AdminHistory.jsx new file mode 100644 index 00000000..f7332891 --- /dev/null +++ b/workspace/zioinfo-web/frontend/src/pages/admin/AdminHistory.jsx @@ -0,0 +1,203 @@ +import { useEffect, useState, useCallback } from 'react'; + +const token = () => localStorage.getItem('admin_token'); +const authFetch = (url, opts = {}) => + fetch(url, { ...opts, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}`, ...opts.headers } }); + +const EMPTY = { year: '', content: '', sortOrder: 0, visible: true }; + +export default function AdminHistory() { + const [items, setItems] = useState([]); + const [modal, setModal] = useState(null); // null | 'create' | 'edit' + const [form, setForm] = useState(EMPTY); + const [editId, setEditId] = useState(null); + const [saving, setSaving] = useState(false); + const [toast, setToast] = useState(null); + const [search, setSearch] = useState(''); + + const showToast = (msg, type = 'success') => { + setToast({ msg, type }); + setTimeout(() => setToast(null), 2500); + }; + + const load = useCallback(() => { + authFetch('/api/admin/history').then(r => r.json()).then(setItems).catch(() => {}); + }, []); + + useEffect(() => { load(); }, [load]); + + const openCreate = () => { setForm(EMPTY); setEditId(null); setModal('create'); }; + const openEdit = h => { setForm({ year: h.year, content: h.content, sortOrder: h.sortOrder, visible: h.visible }); setEditId(h.id); setModal('edit'); }; + const closeModal = () => { setModal(null); setForm(EMPTY); setEditId(null); }; + + const save = async () => { + if (!form.year.trim() || !form.content.trim()) { showToast('연도와 내용은 필수입니다.', 'error'); return; } + setSaving(true); + try { + const url = modal === 'edit' ? `/api/admin/history/${editId}` : '/api/admin/history'; + const meth = modal === 'edit' ? 'PUT' : 'POST'; + const r = await authFetch(url, { method: meth, body: JSON.stringify(form) }); + if (!r.ok) throw new Error('저장 실패'); + showToast(modal === 'edit' ? '수정되었습니다.' : '등록되었습니다.'); + closeModal(); load(); + } catch { showToast('저장 중 오류가 발생했습니다.', 'error'); } + finally { setSaving(false); } + }; + + const del = async id => { + if (!confirm('이 항목을 삭제하시겠습니까?')) return; + await authFetch(`/api/admin/history/${id}`, { method: 'DELETE' }); + showToast('삭제되었습니다.'); + load(); + }; + + const toggleVisible = async h => { + await authFetch(`/api/admin/history/${h.id}`, { + method: 'PUT', + body: JSON.stringify({ ...h, visible: !h.visible }), + }); + load(); + }; + + // 연도별 그룹핑 + const grouped = {}; + items.filter(h => !search || h.year.includes(search) || h.content.includes(search)) + .forEach(h => { (grouped[h.year] = grouped[h.year] || []).push(h); }); + + return ( +
+ {toast && ( +
{toast.msg}
+ )} + +
+

연혁 관리

+ setSearch(e.target.value)} + placeholder="연도 또는 내용 검색..." + style={{ padding:'6px 12px', border:'1px solid #cbd5e1', borderRadius:6, fontSize:13, flex:1, maxWidth:280 }} /> + + +
+ +
+ 총 {items.length}개 항목 +
+ + {/* 연도별 타임라인 테이블 */} + {Object.entries(grouped).map(([year, rows]) => ( +
+
+ {year} + {rows.length}개 항목 +
+ + + + {['순서','내용','노출','수정','삭제'].map(h=>( + + ))} + + + + {rows.sort((a,b)=>a.sortOrder-b.sortOrder).map(h=>( + + + + + + + + ))} + +
{h}
{h.sortOrder} + {!h.visible && 숨김} + {h.content} + + + + + + +
+
+ ))} + + {Object.keys(grouped).length === 0 && ( +
+ {search ? '검색 결과 없음' : '등록된 연혁이 없습니다.'} +
+ )} + + {/* 모달 */} + {modal && ( +
+
+

+ {modal === 'create' ? '연혁 추가' : '연혁 수정'} +

+
+ +