From d0b8ab02b3a166eda0e799568f74ade803a2ac98 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 3 Apr 2026 09:48:39 +0900 Subject: [PATCH 1/3] [agent-pipeline] pipe-20260403004704-gib0 round-1 --- .../src/controllers/sampleController.ts | 56 ++++++++++++ backend-node/src/routes/sampleRoutes.ts | 14 +++ backend-node/src/services/sampleService.ts | 86 +++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 backend-node/src/controllers/sampleController.ts create mode 100644 backend-node/src/routes/sampleRoutes.ts create mode 100644 backend-node/src/services/sampleService.ts diff --git a/backend-node/src/controllers/sampleController.ts b/backend-node/src/controllers/sampleController.ts new file mode 100644 index 00000000..d4f277ca --- /dev/null +++ b/backend-node/src/controllers/sampleController.ts @@ -0,0 +1,56 @@ +import { Request, Response } from "express"; +import { SampleService } from "../services/sampleService"; + +const sampleService = SampleService.getInstance(); + +/** + * 목록 조회 + * GET /api/sample/list?page=1&limit=20 + */ +export const getList = async (req: Request, res: Response): Promise => { + const page = Math.max(1, parseInt(req.query.page as string) || 1); + const limit = Math.min(100, Math.max(1, parseInt(req.query.limit as string) || 20)); + const result = await sampleService.getList(page, limit); + res.status(200).json({ + success: true, + data: result.items, + total: result.total, + page, + limit, + }); +}; + +/** + * 등록 + * POST /api/sample + */ +export const create = async (req: Request, res: Response): Promise => { + const { name, description } = req.body; + if (!name || !description) { + res.status(400).json({ success: false, message: "name, description은 필수입니다." }); + return; + } + const item = await sampleService.create(name, description); + res.status(201).json({ success: true, data: item }); +}; + +/** + * 수정 + * PUT /api/sample/:id + */ +export const update = async (req: Request, res: Response): Promise => { + const { id } = req.params; + const { name, description } = req.body; + const item = await sampleService.update(id, name, description); + res.status(200).json({ success: true, data: item }); +}; + +/** + * 삭제 (soft delete) + * DELETE /api/sample/:id + */ +export const remove = async (req: Request, res: Response): Promise => { + const { id } = req.params; + await sampleService.softDelete(id); + res.status(200).json({ success: true, message: "삭제되었습니다." }); +}; diff --git a/backend-node/src/routes/sampleRoutes.ts b/backend-node/src/routes/sampleRoutes.ts new file mode 100644 index 00000000..22f013fd --- /dev/null +++ b/backend-node/src/routes/sampleRoutes.ts @@ -0,0 +1,14 @@ +import { Router } from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import * as sampleController from "../controllers/sampleController"; + +const router = Router(); + +router.use(authenticateToken); + +router.get("/list", sampleController.getList); +router.post("/", sampleController.create); +router.put("/:id", sampleController.update); +router.delete("/:id", sampleController.remove); + +export default router; diff --git a/backend-node/src/services/sampleService.ts b/backend-node/src/services/sampleService.ts new file mode 100644 index 00000000..204e7b8e --- /dev/null +++ b/backend-node/src/services/sampleService.ts @@ -0,0 +1,86 @@ +import { v4 as uuidv4 } from "uuid"; +import { logger } from "../utils/logger"; + +export interface SampleItem { + id: string; + name: string; + description: string; + is_deleted: boolean; + created_at: string; + updated_at: string; +} + +// 인메모리 저장소 (실제 DB 테이블 없이도 동작) +const store = new Map(); + +export class SampleService { + private static instance: SampleService; + + private constructor() {} + + public static getInstance(): SampleService { + if (!SampleService.instance) { + SampleService.instance = new SampleService(); + } + return SampleService.instance; + } + + /** + * 목록 조회 (페이징, soft delete 제외) + */ + public async getList(page: number, limit: number): Promise<{ items: SampleItem[]; total: number }> { + const active = Array.from(store.values()).filter((item) => !item.is_deleted); + const total = active.length; + const items = active.slice((page - 1) * limit, page * limit); + logger.info(`📋 sample 목록 조회: total=${total}, page=${page}, limit=${limit}`); + return { items, total }; + } + + /** + * 등록 + */ + public async create(name: string, description: string): Promise { + const now = new Date().toISOString(); + const item: SampleItem = { + id: uuidv4(), + name, + description, + is_deleted: false, + created_at: now, + updated_at: now, + }; + store.set(item.id, item); + logger.info(`✅ sample 등록: ${item.id}`); + return item; + } + + /** + * 수정 + */ + public async update(id: string, name?: string, description?: string): Promise { + const item = store.get(id); + if (!item || item.is_deleted) { + throw new Error(`항목을 찾을 수 없습니다: ${id}`); + } + if (name !== undefined) item.name = name; + if (description !== undefined) item.description = description; + item.updated_at = new Date().toISOString(); + store.set(id, item); + logger.info(`✅ sample 수정: ${id}`); + return item; + } + + /** + * Soft delete + */ + public async softDelete(id: string): Promise { + const item = store.get(id); + if (!item || item.is_deleted) { + throw new Error(`항목을 찾을 수 없습니다: ${id}`); + } + item.is_deleted = true; + item.updated_at = new Date().toISOString(); + store.set(id, item); + logger.info(`✅ sample 삭제(soft): ${id}`); + } +} -- 2.49.1 From 6bdfd26fa2658cbfa4fb11b32bb1a0124e399ede Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 3 Apr 2026 09:51:46 +0900 Subject: [PATCH 2/3] [agent-pipeline] pipe-20260403004704-gib0 round-2 --- .../COMPANY_7/master-data/sample/page.tsx | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx diff --git a/frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx b/frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx new file mode 100644 index 00000000..29f30006 --- /dev/null +++ b/frontend/app/(main)/COMPANY_7/master-data/sample/page.tsx @@ -0,0 +1,299 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { Plus, Trash2, Pencil, Download, Loader2, FlaskConical, Save } from "lucide-react"; +import { apiClient } from "@/lib/api/client"; +import { exportToExcel } from "@/lib/utils/excelExport"; +import { toast } from "sonner"; +import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { FullscreenDialog } from "@/components/common/FullscreenDialog"; +import { useConfirmDialog } from "@/components/common/ConfirmDialog"; + +// --- 타입 --- + +interface SampleItem { + id: string; + name: string; + description: string; + created_at: string; + updated_at: string; +} + +// --- 컬럼 정의 --- + +const COLUMNS: DataGridColumn[] = [ + { key: "name", label: "이름", width: "w-[200px]", sortable: true, filterable: true }, + { key: "description", label: "설명", minWidth: "min-w-[300px]", sortable: true, filterable: true }, + { key: "created_at", label: "등록일시", width: "w-[180px]", sortable: true }, + { key: "updated_at", label: "수정일시", width: "w-[180px]", sortable: true }, +]; + +// --- 페이지 --- + +export default function SamplePage() { + const [items, setItems] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); + const [loading, setLoading] = useState(false); + + // 체크박스 다중 선택 + const [checkedIds, setCheckedIds] = useState([]); + + // DynamicSearchFilter 필터 값 + const [activeFilters, setActiveFilters] = useState([]); + + // 등록/수정 모달 + const [modalOpen, setModalOpen] = useState(false); + const [isEdit, setIsEdit] = useState(false); + const [editId, setEditId] = useState(null); + const [saving, setSaving] = useState(false); + const [formName, setFormName] = useState(""); + const [formDesc, setFormDesc] = useState(""); + + // 삭제 확인 다이얼로그 + const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + + // 목록 조회 + const fetchItems = useCallback(async () => { + setLoading(true); + try { + const res = await apiClient.get("/sample/list?page=1&limit=100"); + const data: SampleItem[] = res.data?.data || []; + setItems(data); + } catch (err) { + console.error("샘플 목록 조회 실패:", err); + toast.error("목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchItems(); + }, [fetchItems]); + + // 클라이언트 필터 적용 + useEffect(() => { + if (activeFilters.length === 0) { + setFilteredItems(items); + return; + } + const filtered = items.filter((item) => { + return activeFilters.every((f) => { + const val = String((item as any)[f.columnName] || "").toLowerCase(); + if (f.operator === "contains") return val.includes(f.value.toLowerCase()); + if (f.operator === "equals") return val === f.value.toLowerCase(); + return true; + }); + }); + setFilteredItems(filtered); + }, [items, activeFilters]); + + // 등록 모달 열기 + const openRegister = () => { + setIsEdit(false); + setEditId(null); + setFormName(""); + setFormDesc(""); + setModalOpen(true); + }; + + // 수정 모달 열기 + const openEdit = (item: SampleItem) => { + setIsEdit(true); + setEditId(item.id); + setFormName(item.name); + setFormDesc(item.description); + setModalOpen(true); + }; + + // 저장 (등록 or 수정) + const handleSave = async () => { + if (!formName.trim()) { + toast.error("이름은 필수 입력입니다."); + return; + } + if (!formDesc.trim()) { + toast.error("설명은 필수 입력입니다."); + return; + } + + setSaving(true); + try { + if (isEdit && editId) { + await apiClient.put(`/sample/${editId}`, { name: formName.trim(), description: formDesc.trim() }); + toast.success("수정되었습니다."); + } else { + await apiClient.post("/sample", { name: formName.trim(), description: formDesc.trim() }); + toast.success("등록되었습니다."); + } + setModalOpen(false); + setCheckedIds([]); + await fetchItems(); + } catch (err: any) { + toast.error(err.response?.data?.message || "저장에 실패했습니다."); + } finally { + setSaving(false); + } + }; + + // 삭제 + const handleDelete = async () => { + if (checkedIds.length === 0) { + toast.error("삭제할 항목을 선택해주세요."); + return; + } + const ok = await confirm(`선택한 ${checkedIds.length}건을 삭제하시겠습니까?`, { + description: "삭제된 데이터는 복구할 수 없습니다.", + variant: "destructive", + confirmText: "삭제", + }); + if (!ok) return; + + try { + await Promise.all(checkedIds.map((id) => apiClient.delete(`/sample/${id}`))); + toast.success("삭제되었습니다."); + setCheckedIds([]); + await fetchItems(); + } catch (err) { + toast.error("삭제에 실패했습니다."); + } + }; + + // 수정 버튼: 체크된 항목 1건일 때 활성 + const handleEditButton = () => { + if (checkedIds.length !== 1) return; + const item = filteredItems.find((i) => i.id === checkedIds[0]); + if (item) openEdit(item); + }; + + // 엑셀 다운로드 + const handleExcelDownload = async () => { + if (filteredItems.length === 0) { + toast.error("다운로드할 데이터가 없습니다."); + return; + } + const exportData = filteredItems.map((item) => ({ + 이름: item.name, + 설명: item.description, + 등록일시: item.created_at, + 수정일시: item.updated_at, + })); + await exportToExcel(exportData, "샘플목록.xlsx", "샘플"); + toast.success("엑셀 다운로드 완료"); + }; + + return ( +
+ {/* 검색 필터 */} + + + {/* 메인 테이블 */} +
+ {/* 헤더 버튼 */} +
+
+ + 샘플 목록 + {filteredItems.length}건 +
+
+ + + + +
+
+ + {/* DataGrid */} +
+ openEdit(row as SampleItem)} + emptyMessage="등록된 샘플이 없습니다" + /> +
+
+ + {/* 등록/수정 모달 */} + + + + + } + > +
+
+ + setFormName(e.target.value)} + placeholder="이름을 입력하세요" + className="h-9" + /> +
+
+ +