diff --git a/viewer/effort_js/src/components/DynamicForm.js b/viewer/effort_js/src/components/DynamicForm.js deleted file mode 100644 index b4a0f8b..0000000 --- a/viewer/effort_js/src/components/DynamicForm.js +++ /dev/null @@ -1,111 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -// 컴포넌트 외부에 안정적인 참조를 가진 빈 객체 상수 선언 -const EMPTY_INITIAL_DATA = {}; -export function DynamicForm({ - fields, - onSubmit, - initialData = EMPTY_INITIAL_DATA, // 기본값으로 상수 사용 - submitButtonText = "제출", -}) { - const [formData, setFormData] = useState(initialData); - const [isSubmitting, setIsSubmitting] = useState(false); - useEffect(() => { - // initialData prop이 변경될 때만 폼 데이터를 동기화 - setFormData(initialData); - }, [initialData]); - const handleFormChange = (fieldId, value) => { - setFormData((prev) => ({ ...prev, [fieldId]: value })); - }; - const handleSubmit = async (e) => { - e.preventDefault(); - setIsSubmitting(true); - try { - await onSubmit(formData); - } catch (error) { - console.error("Form submission error:", error); - } finally { - setIsSubmitting(false); - } - }; - const renderField = (field) => { - const commonProps = { - id: field.id, - value: formData[field.id] ?? "", - disabled: field.readOnly, - }; - switch (field.type) { - case "textarea": - return _jsx(Textarea, { - ...commonProps, - onChange: (e) => handleFormChange(field.id, e.target.value), - placeholder: field.readOnly ? "" : `${field.name}...`, - rows: 5, - }); - case "text": - case "number": - return _jsx(Input, { - ...commonProps, - type: field.type, - onChange: (e) => handleFormChange(field.id, e.target.value), - placeholder: field.readOnly ? "" : field.name, - }); - case "select": - return _jsxs(Select, { - value: commonProps.value, - onValueChange: (value) => handleFormChange(field.id, value), - disabled: field.readOnly, - children: [ - _jsx(SelectTrigger, { - id: field.id, - children: _jsx(SelectValue, { - placeholder: `-- ${field.name} 선택 --`, - }), - }), - _jsx(SelectContent, {}), - ], - }); - default: - return _jsxs("p", { - children: [ - "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD544\uB4DC \uD0C0\uC785: ", - field.type, - ], - }); - } - }; - return _jsxs("form", { - onSubmit: handleSubmit, - className: "space-y-4", - children: [ - fields.map((field) => - _jsxs( - "div", - { - className: "space-y-2", - children: [ - _jsx(Label, { htmlFor: field.id, children: field.name }), - renderField(field), - ], - }, - field.id, - ), - ), - _jsx(Button, { - type: "submit", - disabled: isSubmitting, - children: isSubmitting ? "전송 중..." : submitButtonText, - }), - ], - }); -} diff --git a/viewer/effort_js/src/components/DynamicTable.js b/viewer/effort_js/src/components/DynamicTable.js deleted file mode 100644 index 6d95503..0000000 --- a/viewer/effort_js/src/components/DynamicTable.js +++ /dev/null @@ -1,534 +0,0 @@ -import { - jsx as _jsx, - jsxs as _jsxs, - Fragment as _Fragment, -} from "react/jsx-runtime"; -import { - flexRender, - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import { addDays, format } from "date-fns"; -import { - ArrowUpDown, - Calendar as CalendarIcon, - ChevronDown, - ChevronLeft, - ChevronRight, - ChevronsLeft, - ChevronsRight, -} from "lucide-react"; -import { useMemo, useState, Fragment } from "react"; -import { Link, useNavigate } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { Card, CardContent } from "@/components/ui/card"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { cn } from "@/lib/utils"; -const DEFAULT_COLUMN_ORDER = [ - "id", - "title", - "contents", - "issues", - "customer", - "updatedAt", -]; -export function DynamicTable({ - columns: rawColumns, - data, - projectId, - channelId, -}) { - const navigate = useNavigate(); - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [expanded, setExpanded] = useState({}); - const [globalFilter, setGlobalFilter] = useState(""); - const [date, setDate] = useState(); - const columns = useMemo(() => { - const orderedRawColumns = [...rawColumns].sort((a, b) => { - const indexA = DEFAULT_COLUMN_ORDER.indexOf(a.id); - const indexB = DEFAULT_COLUMN_ORDER.indexOf(b.id); - if (indexA === -1 && indexB === -1) return 0; - if (indexA === -1) return 1; - if (indexB === -1) return -1; - return indexA - indexB; - }); - const generatedColumns = orderedRawColumns.map((field) => ({ - accessorKey: field.id, - header: ({ column }) => { - if (field.id === "issues") { - return _jsx("div", { children: field.name }); - } - return _jsxs(Button, { - variant: "ghost", - onClick: () => column.toggleSorting(column.getIsSorted() === "asc"), - children: [ - field.name, - _jsx(ArrowUpDown, { className: "ml-2 h-4 w-4" }), - ], - }); - }, - cell: ({ row }) => { - const value = row.original[field.id]; - switch (field.id) { - case "issues": { - const issues = value; - if (!issues || issues.length === 0) return "N/A"; - return _jsx("div", { - className: "flex flex-col space-y-1", - children: issues.map((issue) => - _jsx( - Link, - { - to: `/issues/${issue.id}`, - className: "text-blue-600 hover:underline", - onClick: (e) => e.stopPropagation(), - children: issue.name, - }, - issue.id, - ), - ), - }); - } - case "title": - return _jsx("div", { - className: "whitespace-normal break-words w-48", - children: String(value ?? "N/A"), - }); - case "contents": { - const content = String(value ?? "N/A"); - const truncated = - content.length > 50 ? `${content.substring(0, 50)}...` : content; - return _jsx("div", { - className: "whitespace-normal break-words w-60", - children: truncated, - }); - } - case "createdAt": - case "updatedAt": - return String(value ?? "N/A").substring(0, 10); - default: - if (typeof value === "object" && value !== null) { - return JSON.stringify(value); - } - return String(value ?? "N/A"); - } - }, - })); - return [ - { - id: "expander", - header: () => null, - cell: ({ row }) => { - return _jsx(Button, { - variant: "ghost", - size: "sm", - onClick: (e) => { - e.stopPropagation(); - row.toggleExpanded(); - }, - children: row.getIsExpanded() ? "▼" : "▶", - }); - }, - }, - ...generatedColumns, - ]; - }, [rawColumns]); - const filteredData = useMemo(() => { - if (!date?.from) { - return data; - } - const fromDate = date.from; - const toDate = date.to ? addDays(date.to, 1) : addDays(fromDate, 1); - return data.filter((item) => { - const itemDate = new Date(item.updatedAt); - return itemDate >= fromDate && itemDate < toDate; - }); - }, [data, date]); - const table = useReactTable({ - data: filteredData, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onExpandedChange: setExpanded, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getExpandedRowModel: getExpandedRowModel(), - initialState: { - pagination: { - pageSize: 20, - }, - }, - state: { - sorting, - columnFilters, - columnVisibility, - expanded, - globalFilter, - }, - onGlobalFilterChange: setGlobalFilter, - }); - const handleRowClick = (feedbackId) => { - navigate( - `/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, - ); - }; - return _jsx(Card, { - children: _jsxs(CardContent, { - children: [ - _jsxs("div", { - className: "flex items-center justify-between py-4", - children: [ - _jsx(Input, { - placeholder: - "\uC804\uCCB4 \uB370\uC774\uD130\uC5D0\uC11C \uAC80\uC0C9...", - value: globalFilter, - onChange: (event) => setGlobalFilter(event.target.value), - className: "max-w-sm", - }), - _jsxs("div", { - className: "flex items-center space-x-2", - children: [ - _jsxs(Popover, { - children: [ - _jsx(PopoverTrigger, { - asChild: true, - children: _jsxs(Button, { - id: "date", - variant: "outline", - className: cn( - "w-[300px] justify-start text-left font-normal", - !date && "text-muted-foreground", - ), - children: [ - _jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }), - date?.from - ? date.to - ? _jsxs(_Fragment, { - children: [ - format(date.from, "LLL dd, y"), - " -", - " ", - format(date.to, "LLL dd, y"), - ], - }) - : format(date.from, "LLL dd, y") - : _jsx("span", { - children: "\uAE30\uAC04 \uC120\uD0DD", - }), - ], - }), - }), - _jsx(PopoverContent, { - className: "w-auto p-0", - align: "end", - children: _jsx(Calendar, { - initialFocus: true, - mode: "range", - defaultMonth: date?.from, - selected: date, - onSelect: setDate, - numberOfMonths: 2, - }), - }), - ], - }), - _jsxs(DropdownMenu, { - children: [ - _jsx(DropdownMenuTrigger, { - asChild: true, - children: _jsxs(Button, { - variant: "outline", - className: "ml-auto", - children: [ - "\uCEEC\uB7FC \uD45C\uC2DC ", - _jsx(ChevronDown, { className: "ml-2 h-4 w-4" }), - ], - }), - }), - _jsx(DropdownMenuContent, { - align: "end", - children: table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return _jsx( - DropdownMenuCheckboxItem, - { - className: "capitalize", - checked: column.getIsVisible(), - onCheckedChange: (value) => - column.toggleVisibility(!!value), - children: column.id, - }, - column.id, - ); - }), - }), - ], - }), - ], - }), - ], - }), - _jsx("div", { - className: "rounded-md border", - children: _jsxs(Table, { - children: [ - _jsx(TableHeader, { - children: table - .getHeaderGroups() - .map((headerGroup) => - _jsx( - TableRow, - { - children: headerGroup.headers.map((header) => - _jsx( - TableHead, - { - style: { width: header.getSize() }, - children: header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - ), - }, - header.id, - ), - ), - }, - headerGroup.id, - ), - ), - }), - _jsx(TableBody, { - children: table.getRowModel().rows?.length - ? table - .getRowModel() - .rows.map((row) => - _jsxs( - Fragment, - { - children: [ - _jsx(TableRow, { - "data-state": row.getIsSelected() && "selected", - onClick: () => - handleRowClick(row.original.id.toString()), - className: "cursor-pointer hover:bg-muted/50", - children: row - .getVisibleCells() - .map((cell) => - _jsx( - TableCell, - { - children: flexRender( - cell.column.columnDef.cell, - cell.getContext(), - ), - }, - cell.id, - ), - ), - }), - row.getIsExpanded() && - _jsx( - TableRow, - { - children: _jsx(TableCell, { - colSpan: columns.length + 1, - children: _jsxs("div", { - className: "p-4 bg-muted rounded-md", - children: [ - _jsx("h4", { - className: "font-bold text-lg", - children: row.original.title, - }), - _jsx("p", { - className: - "mt-2 whitespace-pre-wrap", - children: row.original.contents, - }), - ], - }), - }), - }, - `${row.id}-expanded`, - ), - ], - }, - row.id, - ), - ) - : _jsx(TableRow, { - children: _jsx(TableCell, { - colSpan: columns.length + 1, - className: "h-24 text-center", - children: - "\uD45C\uC2DC\uD560 \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", - }), - }), - }), - ], - }), - }), - _jsxs("div", { - className: "flex items-center justify-between py-4", - children: [ - _jsxs("div", { - className: "flex-1 text-sm text-muted-foreground", - children: [ - "\uCD1D ", - table.getFilteredRowModel().rows.length, - "\uAC1C", - ], - }), - _jsxs("div", { - className: "flex items-center space-x-6", - children: [ - _jsxs("div", { - className: "flex items-center space-x-2", - children: [ - _jsx("p", { - className: "text-sm font-medium", - children: "\uD398\uC774\uC9C0 \uB2F9 \uD589 \uC218", - }), - _jsxs(Select, { - value: `${table.getState().pagination.pageSize}`, - onValueChange: (value) => { - table.setPageSize(Number(value)); - }, - children: [ - _jsx(SelectTrigger, { - className: "h-8 w-[70px]", - children: _jsx(SelectValue, { - placeholder: table.getState().pagination.pageSize, - }), - }), - _jsx(SelectContent, { - side: "top", - children: [20, 30, 50].map((pageSize) => - _jsx( - SelectItem, - { value: `${pageSize}`, children: pageSize }, - pageSize, - ), - ), - }), - ], - }), - ], - }), - _jsxs("div", { - className: - "flex w-[100px] items-center justify-center text-sm font-medium", - children: [ - table.getPageCount(), - " \uD398\uC774\uC9C0 \uC911", - " ", - table.getState().pagination.pageIndex + 1, - ], - }), - _jsxs("div", { - className: "flex items-center space-x-2", - children: [ - _jsxs(Button, { - variant: "outline", - className: "hidden h-8 w-8 p-0 lg:flex", - onClick: () => table.setPageIndex(0), - disabled: !table.getCanPreviousPage(), - children: [ - _jsx("span", { - className: "sr-only", - children: "\uCCAB \uD398\uC774\uC9C0\uB85C", - }), - _jsx(ChevronsLeft, { className: "h-4 w-4" }), - ], - }), - _jsxs(Button, { - variant: "outline", - className: "h-8 w-8 p-0", - onClick: () => table.previousPage(), - disabled: !table.getCanPreviousPage(), - children: [ - _jsx("span", { - className: "sr-only", - children: "\uC774\uC804 \uD398\uC774\uC9C0\uB85C", - }), - _jsx(ChevronLeft, { className: "h-4 w-4" }), - ], - }), - _jsxs(Button, { - variant: "outline", - className: "h-8 w-8 p-0", - onClick: () => table.nextPage(), - disabled: !table.getCanNextPage(), - children: [ - _jsx("span", { - className: "sr-only", - children: "\uB2E4\uC74C \uD398\uC774\uC9C0\uB85C", - }), - _jsx(ChevronRight, { className: "h-4 w-4" }), - ], - }), - _jsxs(Button, { - variant: "outline", - className: "hidden h-8 w-8 p-0 lg:flex", - onClick: () => - table.setPageIndex(table.getPageCount() - 1), - disabled: !table.getCanNextPage(), - children: [ - _jsx("span", { - className: "sr-only", - children: - "\uB9C8\uC9C0\uB9C9 \uD398\uC774\uC9C0\uB85C", - }), - _jsx(ChevronsRight, { className: "h-4 w-4" }), - ], - }), - ], - }), - ], - }), - ], - }), - ], - }), - }); -} -export default DynamicTable; diff --git a/viewer/effort_js/src/components/ErrorDisplay.js b/viewer/effort_js/src/components/ErrorDisplay.js deleted file mode 100644 index df5516b..0000000 --- a/viewer/effort_js/src/components/ErrorDisplay.js +++ /dev/null @@ -1,14 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -export function ErrorDisplay({ message }) { - return _jsxs("div", { - className: "bg-destructive/15 text-destructive p-4 rounded-md text-center", - role: "alert", - children: [ - _jsx("p", { - className: "font-semibold", - children: "\uC624\uB958 \uBC1C\uC0DD", - }), - _jsx("p", { className: "text-sm", children: message }), - ], - }); -} diff --git a/viewer/effort_js/src/components/Header.js b/viewer/effort_js/src/components/Header.js deleted file mode 100644 index 7adb418..0000000 --- a/viewer/effort_js/src/components/Header.js +++ /dev/null @@ -1,57 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { NavLink } from "react-router-dom"; -import { ProjectSelectBox } from "./ProjectSelectBox"; - -import { ThemeSelectBox } from "./ThemeSelectBox"; -import { LanguageSelectBox } from "./LanguageSelectBox"; -import { UserProfileBox } from "./UserProfileBox"; -import { useSettingsStore } from "@/store/useSettingsStore"; - -const menuItems = [ - { name: "Home", path: "/" }, - { name: "Feedback", path: "/feedbacks" }, - { name: "Issue", path: "/issues" }, -]; -export function Header() { - const projectId = useSettingsStore((state) => state.projectId); - const channelId = useSettingsStore((state) => state.channelId); - const getFullPath = (path) => { - if (path === "/") return `/`; // Landing 페이지 경로 예시 - if (path.startsWith("/feedbacks")) { - return `/projects/${projectId}/channels/${channelId}${path}`; - } - if (path.startsWith("/issues")) { - return `/projects/${projectId}${path}`; - } - return path; - }; - return _jsxs("header", { - className: "flex h-16 items-center border-b px-4", - children: [ - _jsx(ProjectSelectBox, { className: "mr-4" }), - _jsx("nav", { - className: "flex items-center space-x-4 lg:space-x-6 flex-1 ml-8", - children: menuItems.map((item) => - _jsx( - NavLink, - { - to: getFullPath(item.path), - className: ({ isActive }) => - `text-sm font-medium transition-colors hover:text-primary ${!isActive ? "text-muted-foreground" : ""}`, - children: item.name, - }, - item.name, - ), - ), - }), - _jsxs("div", { - className: "flex items-center gap-3", - children: [ - _jsx(ThemeSelectBox, {}), - _jsx(LanguageSelectBox, {}), - _jsx(UserProfileBox, {}), - ], - }), - ], - }); -} diff --git a/viewer/effort_js/src/components/MainLayout.js b/viewer/effort_js/src/components/MainLayout.js deleted file mode 100644 index 7ed9cde..0000000 --- a/viewer/effort_js/src/components/MainLayout.js +++ /dev/null @@ -1,13 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -// src/components/MainLayout.tsx -import { Outlet } from "react-router-dom"; -import { Header } from "./Header"; -export function MainLayout() { - return _jsxs("div", { - className: "flex flex-col min-h-screen", - children: [ - _jsx(Header, {}), - _jsx("main", { className: "flex-1 p-6", children: _jsx(Outlet, {}) }), - ], - }); -} diff --git a/viewer/effort_js/src/components/ProjectSelectBox.js b/viewer/effort_js/src/components/ProjectSelectBox.js deleted file mode 100644 index dde2f96..0000000 --- a/viewer/effort_js/src/components/ProjectSelectBox.js +++ /dev/null @@ -1,51 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { useEffect, useState } from "react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { getProjects } from "@/services/project"; -import { useSettingsStore } from "@/store/useSettingsStore"; -export function ProjectSelectBox() { - const [projects, setProjects] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const { projectId, setProjectId } = useSettingsStore(); - useEffect(() => { - getProjects().then((loadedProjects) => { - setProjects(loadedProjects); - // 로드된 프로젝트 목록에 현재 ID가 없으면, 첫 번째 프로젝트로 ID를 설정 - if ( - loadedProjects.length > 0 && - !loadedProjects.find((p) => p.id === projectId) - ) { - setProjectId(loadedProjects[0].id); - } - setIsLoading(false); - }); - }, [projectId, setProjectId]); // 마운트 시 한 번만 실행 - if (isLoading) { - return _jsx("div", { - className: "w-[180px] h-10 bg-muted rounded-md animate-pulse", - }); - } - return _jsxs(Select, { - value: projectId ?? "", - onValueChange: setProjectId, - children: [ - _jsx(SelectTrigger, { - className: "w-[180px]", - children: _jsx(SelectValue, { - placeholder: "\uD504\uB85C\uC81D\uD2B8 \uC120\uD0DD", - }), - }), - _jsx(SelectContent, { - children: projects.map((p) => - _jsx(SelectItem, { value: p.id, children: p.name }, p.id), - ), - }), - ], - }); -} diff --git a/viewer/effort_js/src/components/ThemeSelectBox.js b/viewer/effort_js/src/components/ThemeSelectBox.js deleted file mode 100644 index f3e78ed..0000000 --- a/viewer/effort_js/src/components/ThemeSelectBox.js +++ /dev/null @@ -1,55 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { Moon, Sun, Laptop } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { useSettingsStore } from "@/store/useSettingsStore"; -export function ThemeSelectBox() { - const { setTheme } = useSettingsStore(); - return _jsxs(DropdownMenu, { - children: [ - _jsx(DropdownMenuTrigger, { - asChild: true, - children: _jsxs(Button, { - variant: "ghost", - size: "icon", - children: [ - _jsx(Sun, { - className: - "h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0", - }), - _jsx(Moon, { - className: - "absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100", - }), - _jsx("span", { - className: "sr-only", - children: "\uD14C\uB9C8 \uBCC0\uACBD", - }), - ], - }), - }), - _jsxs(DropdownMenuContent, { - align: "end", - children: [ - _jsxs(DropdownMenuItem, { - onClick: () => setTheme("light"), - children: [_jsx(Sun, { className: "mr-2 h-4 w-4" }), "Light"], - }), - _jsxs(DropdownMenuItem, { - onClick: () => setTheme("dark"), - children: [_jsx(Moon, { className: "mr-2 h-4 w-4" }), "Dark"], - }), - _jsxs(DropdownMenuItem, { - onClick: () => setTheme("system"), - children: [_jsx(Laptop, { className: "mr-2 h-4 w-4" }), "System"], - }), - ], - }), - ], - }); -} diff --git a/viewer/effort_js/src/components/UserProfileBox.js b/viewer/effort_js/src/components/UserProfileBox.js deleted file mode 100644 index 1678b25..0000000 --- a/viewer/effort_js/src/components/UserProfileBox.js +++ /dev/null @@ -1,16 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { CircleUser } from "lucide-react"; -import { Button } from "./ui/button"; -export function UserProfileBox() { - return _jsxs(Button, { - variant: "ghost", - size: "icon", - children: [ - _jsx(CircleUser, {}), - _jsx("span", { - className: "sr-only", - children: "\uC0AC\uC6A9\uC790 \uD504\uB85C\uD544", - }), - ], - }); -} diff --git a/viewer/effort_js/src/components/providers/ThemeProvider.js b/viewer/effort_js/src/components/providers/ThemeProvider.js deleted file mode 100644 index 87fcb89..0000000 --- a/viewer/effort_js/src/components/providers/ThemeProvider.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime"; -import { useEffect } from "react"; -import { useSettingsStore } from "@/store/useSettingsStore"; -export function ThemeProvider({ children }) { - const theme = useSettingsStore((state) => state.theme); - useEffect(() => { - const root = window.document.documentElement; - root.classList.remove("light", "dark"); - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light"; - root.classList.add(systemTheme); - return; - } - root.classList.add(theme); - }, [theme]); - return _jsx(_Fragment, { children: children }); -} diff --git a/viewer/effort_js/src/components/ui/button.js b/viewer/effort_js/src/components/ui/button.js deleted file mode 100644 index 47649a1..0000000 --- a/viewer/effort_js/src/components/ui/button.js +++ /dev/null @@ -1,43 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import { Slot } from "@radix-ui/react-slot"; -import { cva } from "class-variance-authority"; -import { cn } from "@/lib/utils"; -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); -function Button({ className, variant, size, asChild = false, ...props }) { - const Comp = asChild ? Slot : "button"; - return _jsx(Comp, { - "data-slot": "button", - className: cn(buttonVariants({ variant, size, className })), - ...props, - }); -} -export { Button, buttonVariants }; diff --git a/viewer/effort_js/src/components/ui/calendar.js b/viewer/effort_js/src/components/ui/calendar.js deleted file mode 100644 index 4efd963..0000000 --- a/viewer/effort_js/src/components/ui/calendar.js +++ /dev/null @@ -1,187 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import * as React from "react"; -import { - ChevronDownIcon, - ChevronLeftIcon, - ChevronRightIcon, -} from "lucide-react"; -import { DayPicker, getDefaultClassNames } from "react-day-picker"; -import { cn } from "@/lib/utils"; -import { Button, buttonVariants } from "@/components/ui/button"; -function Calendar({ - className, - classNames, - showOutsideDays = true, - captionLayout = "label", - buttonVariant = "ghost", - formatters, - components, - ...props -}) { - const defaultClassNames = getDefaultClassNames(); - return _jsx(DayPicker, { - showOutsideDays: showOutsideDays, - className: cn( - "bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", - String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, - String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, - className, - ), - captionLayout: captionLayout, - formatters: { - formatMonthDropdown: (date) => - date.toLocaleString("default", { month: "short" }), - ...formatters, - }, - classNames: { - root: cn("w-fit", defaultClassNames.root), - months: cn( - "relative flex flex-col gap-4 md:flex-row", - defaultClassNames.months, - ), - month: cn("flex w-full flex-col gap-4", defaultClassNames.month), - nav: cn( - "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", - defaultClassNames.nav, - ), - button_previous: cn( - buttonVariants({ variant: buttonVariant }), - "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", - defaultClassNames.button_previous, - ), - button_next: cn( - buttonVariants({ variant: buttonVariant }), - "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", - defaultClassNames.button_next, - ), - month_caption: cn( - "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", - defaultClassNames.month_caption, - ), - dropdowns: cn( - "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", - defaultClassNames.dropdowns, - ), - dropdown_root: cn( - "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border", - defaultClassNames.dropdown_root, - ), - dropdown: cn( - "bg-popover absolute inset-0 opacity-0", - defaultClassNames.dropdown, - ), - caption_label: cn( - "select-none font-medium", - captionLayout === "label" - ? "text-sm" - : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", - defaultClassNames.caption_label, - ), - table: "w-full border-collapse", - weekdays: cn("flex", defaultClassNames.weekdays), - weekday: cn( - "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", - defaultClassNames.weekday, - ), - week: cn("mt-2 flex w-full", defaultClassNames.week), - week_number_header: cn( - "w-[--cell-size] select-none", - defaultClassNames.week_number_header, - ), - week_number: cn( - "text-muted-foreground select-none text-[0.8rem]", - defaultClassNames.week_number, - ), - day: cn( - "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", - defaultClassNames.day, - ), - range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start), - range_middle: cn("rounded-none", defaultClassNames.range_middle), - range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end), - today: cn( - "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", - defaultClassNames.today, - ), - outside: cn( - "text-muted-foreground aria-selected:text-muted-foreground", - defaultClassNames.outside, - ), - disabled: cn( - "text-muted-foreground opacity-50", - defaultClassNames.disabled, - ), - hidden: cn("invisible", defaultClassNames.hidden), - ...classNames, - }, - components: { - Root: ({ className, rootRef, ...props }) => { - return _jsx("div", { - "data-slot": "calendar", - ref: rootRef, - className: cn(className), - ...props, - }); - }, - Chevron: ({ className, orientation, ...props }) => { - if (orientation === "left") { - return _jsx(ChevronLeftIcon, { - className: cn("size-4", className), - ...props, - }); - } - if (orientation === "right") { - return _jsx(ChevronRightIcon, { - className: cn("size-4", className), - ...props, - }); - } - return _jsx(ChevronDownIcon, { - className: cn("size-4", className), - ...props, - }); - }, - DayButton: CalendarDayButton, - WeekNumber: ({ children, ...props }) => { - return _jsx("td", { - ...props, - children: _jsx("div", { - className: - "flex size-[--cell-size] items-center justify-center text-center", - children: children, - }), - }); - }, - ...components, - }, - ...props, - }); -} -function CalendarDayButton({ className, day, modifiers, ...props }) { - const defaultClassNames = getDefaultClassNames(); - const ref = React.useRef(null); - React.useEffect(() => { - if (modifiers.focused) ref.current?.focus(); - }, [modifiers.focused]); - return _jsx(Button, { - ref: ref, - variant: "ghost", - size: "icon", - "data-day": day.date.toLocaleDateString(), - "data-selected-single": - modifiers.selected && - !modifiers.range_start && - !modifiers.range_end && - !modifiers.range_middle, - "data-range-start": modifiers.range_start, - "data-range-end": modifiers.range_end, - "data-range-middle": modifiers.range_middle, - className: cn( - "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70", - defaultClassNames.day, - className, - ), - ...props, - }); -} -export { Calendar, CalendarDayButton }; diff --git a/viewer/effort_js/src/components/ui/card.js b/viewer/effort_js/src/components/ui/card.js deleted file mode 100644 index a6692d5..0000000 --- a/viewer/effort_js/src/components/ui/card.js +++ /dev/null @@ -1,69 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import { cn } from "@/lib/utils"; -function Card({ className, ...props }) { - return _jsx("div", { - "data-slot": "card", - className: cn( - "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", - className, - ), - ...props, - }); -} -function CardHeader({ className, ...props }) { - return _jsx("div", { - "data-slot": "card-header", - className: cn( - "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", - className, - ), - ...props, - }); -} -function CardTitle({ className, ...props }) { - return _jsx("div", { - "data-slot": "card-title", - className: cn("leading-none font-semibold", className), - ...props, - }); -} -function CardDescription({ className, ...props }) { - return _jsx("div", { - "data-slot": "card-description", - className: cn("text-muted-foreground text-sm", className), - ...props, - }); -} -function CardAction({ className, ...props }) { - return _jsx("div", { - "data-slot": "card-action", - className: cn( - "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className, - ), - ...props, - }); -} -function CardContent({ className, ...props }) { - return _jsx("div", { - "data-slot": "card-content", - className: cn("px-6", className), - ...props, - }); -} -function CardFooter({ className, ...props }) { - return _jsx("div", { - "data-slot": "card-footer", - className: cn("flex items-center px-6 [.border-t]:pt-6", className), - ...props, - }); -} -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -}; diff --git a/viewer/effort_js/src/components/ui/dropdown-menu.js b/viewer/effort_js/src/components/ui/dropdown-menu.js deleted file mode 100644 index 3649635..0000000 --- a/viewer/effort_js/src/components/ui/dropdown-menu.js +++ /dev/null @@ -1,165 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import * as React from "react"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { cn } from "@/lib/utils"; -import { - CheckIcon, - ChevronRightIcon, - DotFilledIcon, -} from "@radix-ui/react-icons"; -const DropdownMenu = DropdownMenuPrimitive.Root; -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; -const DropdownMenuGroup = DropdownMenuPrimitive.Group; -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; -const DropdownMenuSub = DropdownMenuPrimitive.Sub; -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; -const DropdownMenuSubTrigger = React.forwardRef( - ({ className, inset, children, ...props }, ref) => - _jsxs(DropdownMenuPrimitive.SubTrigger, { - ref: ref, - className: cn( - "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - inset && "pl-8", - className, - ), - ...props, - children: [children, _jsx(ChevronRightIcon, { className: "ml-auto" })], - }), -); -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName; -const DropdownMenuSubContent = React.forwardRef( - ({ className, ...props }, ref) => - _jsx(DropdownMenuPrimitive.SubContent, { - ref: ref, - className: cn( - "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]", - className, - ), - ...props, - }), -); -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName; -const DropdownMenuContent = React.forwardRef( - ({ className, sideOffset = 4, ...props }, ref) => - _jsx(DropdownMenuPrimitive.Portal, { - children: _jsx(DropdownMenuPrimitive.Content, { - ref: ref, - sideOffset: sideOffset, - className: cn( - "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]", - className, - ), - ...props, - }), - }), -); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; -const DropdownMenuItem = React.forwardRef( - ({ className, inset, ...props }, ref) => - _jsx(DropdownMenuPrimitive.Item, { - ref: ref, - className: cn( - "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", - inset && "pl-8", - className, - ), - ...props, - }), -); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; -const DropdownMenuCheckboxItem = React.forwardRef( - ({ className, children, checked, ...props }, ref) => - _jsxs(DropdownMenuPrimitive.CheckboxItem, { - ref: ref, - className: cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className, - ), - checked: checked, - ...props, - children: [ - _jsx("span", { - className: - "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", - children: _jsx(DropdownMenuPrimitive.ItemIndicator, { - children: _jsx(CheckIcon, { className: "h-4 w-4" }), - }), - }), - children, - ], - }), -); -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; -const DropdownMenuRadioItem = React.forwardRef( - ({ className, children, ...props }, ref) => - _jsxs(DropdownMenuPrimitive.RadioItem, { - ref: ref, - className: cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className, - ), - ...props, - children: [ - _jsx("span", { - className: - "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", - children: _jsx(DropdownMenuPrimitive.ItemIndicator, { - children: _jsx(DotFilledIcon, { - className: "h-2 w-2 fill-current", - }), - }), - }), - children, - ], - }), -); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; -const DropdownMenuLabel = React.forwardRef( - ({ className, inset, ...props }, ref) => - _jsx(DropdownMenuPrimitive.Label, { - ref: ref, - className: cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className, - ), - ...props, - }), -); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; -const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => - _jsx(DropdownMenuPrimitive.Separator, { - ref: ref, - className: cn("-mx-1 my-1 h-px bg-muted", className), - ...props, - }), -); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; -const DropdownMenuShortcut = ({ className, ...props }) => { - return _jsx("span", { - className: cn("ml-auto text-xs tracking-widest opacity-60", className), - ...props, - }); -}; -DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -}; diff --git a/viewer/effort_js/src/components/ui/input.js b/viewer/effort_js/src/components/ui/input.js deleted file mode 100644 index dabef4c..0000000 --- a/viewer/effort_js/src/components/ui/input.js +++ /dev/null @@ -1,16 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import { cn } from "@/lib/utils"; -function Input({ className, type, ...props }) { - return _jsx("input", { - type: type, - "data-slot": "input", - className: cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - className, - ), - ...props, - }); -} -export { Input }; diff --git a/viewer/effort_js/src/components/ui/label.js b/viewer/effort_js/src/components/ui/label.js deleted file mode 100644 index 23d29b8..0000000 --- a/viewer/effort_js/src/components/ui/label.js +++ /dev/null @@ -1,17 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { cva } from "class-variance-authority"; -import { cn } from "@/lib/utils"; -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", -); -const Label = React.forwardRef(({ className, ...props }, ref) => - _jsx(LabelPrimitive.Root, { - ref: ref, - className: cn(labelVariants(), className), - ...props, - }), -); -Label.displayName = LabelPrimitive.Root.displayName; -export { Label }; diff --git a/viewer/effort_js/src/components/ui/popover.js b/viewer/effort_js/src/components/ui/popover.js deleted file mode 100644 index a8c9942..0000000 --- a/viewer/effort_js/src/components/ui/popover.js +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; -import { jsx as _jsx } from "react/jsx-runtime"; -import * as React from "react"; -import * as PopoverPrimitive from "@radix-ui/react-popover"; -import { cn } from "@/lib/utils"; -const Popover = PopoverPrimitive.Root; -const PopoverTrigger = PopoverPrimitive.Trigger; -const PopoverAnchor = PopoverPrimitive.Anchor; -const PopoverContent = React.forwardRef( - ({ className, align = "center", sideOffset = 4, ...props }, ref) => - _jsx(PopoverPrimitive.Portal, { - children: _jsx(PopoverPrimitive.Content, { - ref: ref, - align: align, - sideOffset: sideOffset, - className: cn( - "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]", - className, - ), - ...props, - }), - }), -); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/viewer/effort_js/src/components/ui/select.js b/viewer/effort_js/src/components/ui/select.js deleted file mode 100644 index 6bf1ba5..0000000 --- a/viewer/effort_js/src/components/ui/select.js +++ /dev/null @@ -1,135 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import * as React from "react"; -import * as SelectPrimitive from "@radix-ui/react-select"; -import { cn } from "@/lib/utils"; -import { - CheckIcon, - ChevronDownIcon, - ChevronUpIcon, -} from "@radix-ui/react-icons"; -const Select = SelectPrimitive.Root; -const SelectGroup = SelectPrimitive.Group; -const SelectValue = SelectPrimitive.Value; -const SelectTrigger = React.forwardRef( - ({ className, children, ...props }, ref) => - _jsxs(SelectPrimitive.Trigger, { - ref: ref, - className: cn( - "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", - className, - ), - ...props, - children: [ - children, - _jsx(SelectPrimitive.Icon, { - asChild: true, - children: _jsx(ChevronDownIcon, { className: "h-4 w-4 opacity-50" }), - }), - ], - }), -); -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; -const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => - _jsx(SelectPrimitive.ScrollUpButton, { - ref: ref, - className: cn( - "flex cursor-default items-center justify-center py-1", - className, - ), - ...props, - children: _jsx(ChevronUpIcon, { className: "h-4 w-4" }), - }), -); -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; -const SelectScrollDownButton = React.forwardRef( - ({ className, ...props }, ref) => - _jsx(SelectPrimitive.ScrollDownButton, { - ref: ref, - className: cn( - "flex cursor-default items-center justify-center py-1", - className, - ), - ...props, - children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }), - }), -); -SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName; -const SelectContent = React.forwardRef( - ({ className, children, position = "popper", ...props }, ref) => - _jsx(SelectPrimitive.Portal, { - children: _jsxs(SelectPrimitive.Content, { - ref: ref, - className: cn( - "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]", - position === "popper" && - "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", - className, - ), - position: position, - ...props, - children: [ - _jsx(SelectScrollUpButton, {}), - _jsx(SelectPrimitive.Viewport, { - className: cn( - "p-1", - position === "popper" && - "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]", - ), - children: children, - }), - _jsx(SelectScrollDownButton, {}), - ], - }), - }), -); -SelectContent.displayName = SelectPrimitive.Content.displayName; -const SelectLabel = React.forwardRef(({ className, ...props }, ref) => - _jsx(SelectPrimitive.Label, { - ref: ref, - className: cn("px-2 py-1.5 text-sm font-semibold", className), - ...props, - }), -); -SelectLabel.displayName = SelectPrimitive.Label.displayName; -const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => - _jsxs(SelectPrimitive.Item, { - ref: ref, - className: cn( - "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className, - ), - ...props, - children: [ - _jsx("span", { - className: - "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", - children: _jsx(SelectPrimitive.ItemIndicator, { - children: _jsx(CheckIcon, { className: "h-4 w-4" }), - }), - }), - _jsx(SelectPrimitive.ItemText, { children: children }), - ], - }), -); -SelectItem.displayName = SelectPrimitive.Item.displayName; -const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => - _jsx(SelectPrimitive.Separator, { - ref: ref, - className: cn("-mx-1 my-1 h-px bg-muted", className), - ...props, - }), -); -SelectSeparator.displayName = SelectPrimitive.Separator.displayName; -export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, -}; diff --git a/viewer/effort_js/src/components/ui/separator.js b/viewer/effort_js/src/components/ui/separator.js deleted file mode 100644 index cfebd48..0000000 --- a/viewer/effort_js/src/components/ui/separator.js +++ /dev/null @@ -1,21 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "@/lib/utils"; -function Separator({ - className, - orientation = "horizontal", - decorative = true, - ...props -}) { - return _jsx(SeparatorPrimitive.Root, { - "data-slot": "separator", - decorative: decorative, - orientation: orientation, - className: cn( - "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", - className, - ), - ...props, - }); -} -export { Separator }; diff --git a/viewer/effort_js/src/components/ui/table.js b/viewer/effort_js/src/components/ui/table.js deleted file mode 100644 index a5d5f6f..0000000 --- a/viewer/effort_js/src/components/ui/table.js +++ /dev/null @@ -1,92 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import * as React from "react"; -import { cn } from "@/lib/utils"; -const Table = React.forwardRef(({ className, ...props }, ref) => - _jsx("div", { - className: "relative w-full overflow-auto", - children: _jsx("table", { - ref: ref, - className: cn("w-full caption-bottom text-sm", className), - ...props, - }), - }), -); -Table.displayName = "Table"; -const TableHeader = React.forwardRef(({ className, ...props }, ref) => - _jsx("thead", { - ref: ref, - className: cn("[&_tr]:border-b", className), - ...props, - }), -); -TableHeader.displayName = "TableHeader"; -const TableBody = React.forwardRef(({ className, ...props }, ref) => - _jsx("tbody", { - ref: ref, - className: cn("[&_tr:last-child]:border-0", className), - ...props, - }), -); -TableBody.displayName = "TableBody"; -const TableFooter = React.forwardRef(({ className, ...props }, ref) => - _jsx("tfoot", { - ref: ref, - className: cn( - "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", - className, - ), - ...props, - }), -); -TableFooter.displayName = "TableFooter"; -const TableRow = React.forwardRef(({ className, ...props }, ref) => - _jsx("tr", { - ref: ref, - className: cn( - "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", - className, - ), - ...props, - }), -); -TableRow.displayName = "TableRow"; -const TableHead = React.forwardRef(({ className, ...props }, ref) => - _jsx("th", { - ref: ref, - className: cn( - "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className, - ), - ...props, - }), -); -TableHead.displayName = "TableHead"; -const TableCell = React.forwardRef(({ className, ...props }, ref) => - _jsx("td", { - ref: ref, - className: cn( - "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className, - ), - ...props, - }), -); -TableCell.displayName = "TableCell"; -const TableCaption = React.forwardRef(({ className, ...props }, ref) => - _jsx("caption", { - ref: ref, - className: cn("mt-4 text-sm text-muted-foreground", className), - ...props, - }), -); -TableCaption.displayName = "TableCaption"; -export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, -}; diff --git a/viewer/effort_js/src/components/ui/textarea.js b/viewer/effort_js/src/components/ui/textarea.js deleted file mode 100644 index 8b37063..0000000 --- a/viewer/effort_js/src/components/ui/textarea.js +++ /dev/null @@ -1,13 +0,0 @@ -import { jsx as _jsx } from "react/jsx-runtime"; -import { cn } from "@/lib/utils"; -function Textarea({ className, ...props }) { - return _jsx("textarea", { - "data-slot": "textarea", - className: cn( - "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - className, - ), - ...props, - }); -} -export { Textarea }; diff --git a/viewer/effort_js/src/lib/utils.js b/viewer/effort_js/src/lib/utils.js deleted file mode 100644 index 80bdacd..0000000 --- a/viewer/effort_js/src/lib/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; -export function cn(...inputs) { - return twMerge(clsx(inputs)); -} diff --git a/viewer/effort_js/src/pages/FeedbackCreatePage.js b/viewer/effort_js/src/pages/FeedbackCreatePage.js deleted file mode 100644 index b4fe947..0000000 --- a/viewer/effort_js/src/pages/FeedbackCreatePage.js +++ /dev/null @@ -1,108 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { DynamicForm } from "@/components/DynamicForm"; -import { getFeedbackFields, createFeedback } from "@/services/feedback"; -import { ErrorDisplay } from "@/components/ErrorDisplay"; -import { Separator } from "@/components/ui/separator"; -export function FeedbackCreatePage() { - const navigate = useNavigate(); - const [fields, setFields] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [submitMessage, setSubmitMessage] = useState(null); - // TODO: projectId와 channelId는 URL 파라미터나 컨텍스트에서 가져와야 합니다. - const projectId = "1"; - const channelId = "4"; - useEffect(() => { - const fetchFields = async () => { - try { - setLoading(true); - const fieldsData = await getFeedbackFields(projectId, channelId); - // 사용자에게 보여주지 않을 필드 목록 - const hiddenFields = ["id", "createdAt", "updatedAt", "issues"]; - const processedFields = fieldsData - .filter((field) => !hiddenFields.includes(field.id)) - .map((field) => { - // 'contents' 필드를 항상 textarea로 처리 - if (field.id === "contents") { - return { ...field, type: "textarea" }; - } - return field; - }); - setFields(processedFields); - } catch (err) { - if (err instanceof Error) { - setError(err.message); - } else { - setError("알 수 없는 오류가 발생했습니다."); - } - } finally { - setLoading(false); - } - }; - fetchFields(); - }, []); - const handleSubmit = async (formData) => { - try { - setError(null); - setSubmitMessage(null); - const requestData = { - ...formData, - issueNames: [], - }; - await createFeedback(projectId, channelId, requestData); - setSubmitMessage( - "피드백이 성공적으로 등록되었습니다! 곧 목록으로 돌아갑니다.", - ); - // 2초 후 목록 페이지로 이동 - setTimeout(() => { - navigate(`/projects/${projectId}/channels/${channelId}/feedbacks`); - }, 2000); - } catch (err) { - if (err instanceof Error) { - setError(err.message); - } else { - setError("피드백 등록 중 알 수 없는 오류가 발생했습니다."); - } - throw err; - } - }; - if (loading) { - return _jsx("div", { - children: "\uD3FC\uC744 \uBD88\uB7EC\uC624\uB294 \uC911...", - }); - } - return _jsxs("div", { - className: "container mx-auto p-4", - children: [ - _jsxs("div", { - className: "space-y-2 mb-6", - children: [ - _jsx("h1", { - className: "text-2xl font-bold", - children: "\uD53C\uB4DC\uBC31 \uC791\uC131", - }), - _jsx("p", { - className: "text-muted-foreground", - children: - "\uC544\uB798 \uD3FC\uC744 \uC791\uC131\uD558\uC5EC \uD53C\uB4DC\uBC31\uC744 \uC81C\uCD9C\uD574\uC8FC\uC138\uC694.", - }), - ], - }), - _jsx(Separator, {}), - _jsxs("div", { - className: "mt-6", - children: [ - _jsx(DynamicForm, { fields: fields, onSubmit: handleSubmit }), - error && _jsx(ErrorDisplay, { message: error }), - submitMessage && - _jsx("div", { - className: "mt-4 p-3 bg-green-100 text-green-800 rounded-md", - children: submitMessage, - }), - ], - }), - ], - }); -} diff --git a/viewer/effort_js/src/pages/FeedbackDetailPage.js b/viewer/effort_js/src/pages/FeedbackDetailPage.js deleted file mode 100644 index f587f58..0000000 --- a/viewer/effort_js/src/pages/FeedbackDetailPage.js +++ /dev/null @@ -1,155 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { useState, useEffect, useMemo } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import { DynamicForm } from "@/components/DynamicForm"; -import { - getFeedbackFields, - getFeedbackById, - updateFeedback, -} from "@/services/feedback"; -import { ErrorDisplay } from "@/components/ErrorDisplay"; -import { Separator } from "@/components/ui/separator"; -export function FeedbackDetailPage() { - const { projectId, channelId, feedbackId } = useParams(); - const navigate = useNavigate(); - const [fields, setFields] = useState([]); - const [feedback, setFeedback] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); - const initialData = useMemo(() => feedback ?? {}, [feedback]); - useEffect(() => { - const fetchData = async () => { - if (!projectId || !channelId || !feedbackId) return; - try { - setLoading(true); - const [fieldsData, feedbackData] = await Promise.all([ - getFeedbackFields(projectId, channelId), - getFeedbackById(projectId, channelId, feedbackId), - ]); - // 폼에서 숨길 필드 목록 - const hiddenFields = [ - "id", - "createdAt", - "updatedAt", - "issues", - "screenshot", - ]; - const processedFields = fieldsData - .filter((field) => !hiddenFields.includes(field.id)) - .map((field) => { - // 'contents' 필드는 항상 textarea로 - if (field.id === "contents") { - return { ...field, type: "textarea" }; - } - // 'customer' 필드는 읽기 전용으로 - if (field.id === "customer") { - return { ...field, readOnly: true }; - } - return field; - }); - setFields(processedFields); - setFeedback(feedbackData); - } catch (err) { - if (err instanceof Error) { - setError(err.message); - } else { - setError("데이터를 불러오는 중 알 수 없는 오류가 발생했습니다."); - } - } finally { - setLoading(false); - } - }; - fetchData(); - }, [projectId, channelId, feedbackId]); - const handleSubmit = async (formData) => { - if (!projectId || !channelId || !feedbackId) return; - try { - setError(null); - setSuccessMessage(null); - // API에 전송할 데이터 정제 (수정 가능한 필드만 포함) - const dataToUpdate = {}; - fields.forEach((field) => { - if (!field.readOnly && formData[field.id] !== undefined) { - dataToUpdate[field.id] = formData[field.id]; - } - }); - console.log("Updating with data:", dataToUpdate); // [Debug] - await updateFeedback(projectId, channelId, feedbackId, dataToUpdate); - setSuccessMessage( - "피드백이 성공적으로 수정되었습니다! 곧 목록으로 돌아갑니다.", - ); - setTimeout(() => { - navigate(`/projects/${projectId}/channels/${channelId}/feedbacks`); - }, 2000); - } catch (err) { - if (err instanceof Error) { - setError(err.message); - } else { - setError("피드백 수정 중 알 수 없는 오류가 발생했습니다."); - } - throw err; - } - }; - if (loading) { - return _jsx("div", { children: "\uB85C\uB529 \uC911..." }); - } - if (error) { - return _jsx(ErrorDisplay, { message: error }); - } - return _jsxs("div", { - className: "container mx-auto p-4", - children: [ - _jsxs("div", { - className: "space-y-2 mb-6", - children: [ - _jsx("h1", { - className: "text-2xl font-bold", - children: "\uD53C\uB4DC\uBC31 \uC0C1\uC138 \uBC0F \uC218\uC815", - }), - _jsx("p", { - className: "text-muted-foreground", - children: - "\uD53C\uB4DC\uBC31 \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uACE0 \uC218\uC815\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.", - }), - ], - }), - _jsx(Separator, {}), - _jsxs("div", { - className: "mt-6", - children: [ - _jsxs("div", { - className: - "flex justify-between items-center mb-4 p-3 bg-slate-50 rounded-md", - children: [ - _jsxs("span", { - className: "text-sm font-medium text-slate-600", - children: ["ID: ", feedback?.id], - }), - _jsxs("span", { - className: "text-sm text-slate-500", - children: [ - "\uC0DD\uC131\uC77C: ", - feedback?.createdAt - ? new Date(feedback.createdAt).toLocaleString("ko-KR") - : "N/A", - ], - }), - ], - }), - _jsx(DynamicForm, { - fields: fields, - initialData: initialData, - onSubmit: handleSubmit, - submitButtonText: "\uC218\uC815\uD558\uAE30", - }), - successMessage && - _jsx("div", { - className: "mt-4 p-3 bg-green-100 text-green-800 rounded-md", - children: successMessage, - }), - ], - }), - ], - }); -} diff --git a/viewer/effort_js/src/pages/FeedbackListPage.js b/viewer/effort_js/src/pages/FeedbackListPage.js deleted file mode 100644 index ec3aa66..0000000 --- a/viewer/effort_js/src/pages/FeedbackListPage.js +++ /dev/null @@ -1,72 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -import { useState, useEffect } from "react"; -import { Link } from "react-router-dom"; -import { DynamicTable } from "@/components/DynamicTable"; -import { getFeedbacks, getFeedbackFields } from "@/services/feedback"; -import { ErrorDisplay } from "@/components/ErrorDisplay"; -import { Button } from "@/components/ui/button"; -export function FeedbackListPage() { - const [fields, setFields] = useState([]); - const [feedbacks, setFeedbacks] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - // TODO: projectId와 channelId는 URL 파라미터나 컨텍스트에서 가져와야 합니다. - const projectId = "1"; - const channelId = "4"; - useEffect(() => { - const fetchFieldsAndFeedbacks = async () => { - try { - setLoading(true); - const fieldsData = await getFeedbackFields(projectId, channelId); - setFields(fieldsData); - try { - const feedbacksData = await getFeedbacks(projectId, channelId); - setFeedbacks(feedbacksData); - } catch (feedbackError) { - console.error("Failed to fetch feedbacks:", feedbackError); - setError("피드백 목록을 불러오는 데 실패했습니다."); - } - } catch (fieldsError) { - if (fieldsError instanceof Error) { - setError(fieldsError.message); - } else { - setError("테이블 구조를 불러오는 데 실패했습니다."); - } - } finally { - setLoading(false); - } - }; - fetchFieldsAndFeedbacks(); - }, []); - if (loading) { - return _jsx("div", { children: "\uB85C\uB529 \uC911..." }); - } - return _jsxs("div", { - className: "container mx-auto p-4", - children: [ - _jsxs("div", { - className: "flex justify-between items-center mb-4", - children: [ - _jsx("h1", { - className: "text-2xl font-bold", - children: "\uD53C\uB4DC\uBC31 \uBAA9\uB85D", - }), - _jsx(Button, { - asChild: true, - children: _jsx(Link, { - to: "new", - children: "\uC0C8 \uD53C\uB4DC\uBC31 \uC791\uC131", - }), - }), - ], - }), - error && _jsx(ErrorDisplay, { message: error }), - _jsx(DynamicTable, { - columns: fields, - data: feedbacks, - projectId: projectId, - channelId: channelId, - }), - ], - }); -} diff --git a/viewer/effort_js/src/pages/IssueViewerPage.js b/viewer/effort_js/src/pages/IssueViewerPage.js deleted file mode 100644 index 799b46f..0000000 --- a/viewer/effort_js/src/pages/IssueViewerPage.js +++ /dev/null @@ -1,127 +0,0 @@ -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; -// src/pages/IssueViewerPage.tsx -import { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; -import { getIssues } from "@/services/issue"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { ErrorDisplay } from "@/components/ErrorDisplay"; -// 테이블 헤더 정의 -const issueTableHeaders = [ - { key: "title", label: "Title" }, - { key: "feedbackCount", label: "Feedback Count" }, - { key: "description", label: "Description" }, - { key: "status", label: "Status" }, - { key: "createdAt", label: "Created" }, - { key: "updatedAt", label: "Updated" }, - { key: "category", label: "Category" }, -]; -export function IssueViewerPage() { - const { projectId } = useParams(); - const [issues, setIssues] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - useEffect(() => { - if (!projectId) return; - setLoading(true); - setError(null); - getIssues(projectId) - .then(setIssues) - .catch((err) => setError(err.message)) - .finally(() => setLoading(false)); - }, [projectId]); - return _jsxs("div", { - className: "container mx-auto p-4 md:p-8", - children: [ - _jsxs("header", { - className: "mb-8", - children: [ - _jsx("h1", { - className: "text-3xl font-bold tracking-tight", - children: "\uC774\uC288 \uBDF0\uC5B4", - }), - _jsxs("p", { - className: "text-muted-foreground mt-1", - children: ["\uD504\uB85C\uC81D\uD2B8: ", projectId], - }), - ], - }), - error && _jsx(ErrorDisplay, { message: error }), - _jsxs(Card, { - children: [ - _jsx(CardHeader, { - children: _jsx(CardTitle, { - children: "\uC774\uC288 \uBAA9\uB85D", - }), - }), - _jsxs(CardContent, { - children: [ - loading && - _jsx("p", { - className: "text-center", - children: "\uB85C\uB529 \uC911...", - }), - !loading && - _jsx("div", { - className: "border rounded-md", - children: _jsxs(Table, { - children: [ - _jsx(TableHeader, { - children: _jsx(TableRow, { - children: issueTableHeaders.map((header) => - _jsx( - TableHead, - { children: header.label }, - header.key, - ), - ), - }), - }), - _jsx(TableBody, { - children: - issues.length > 0 - ? issues.map((issue) => - _jsx( - TableRow, - { - children: issueTableHeaders.map((header) => - _jsx( - TableCell, - { - children: String( - issue[header.key] ?? "", - ), - }, - `${issue.id}-${header.key}`, - ), - ), - }, - issue.id, - ), - ) - : _jsx(TableRow, { - children: _jsx(TableCell, { - colSpan: issueTableHeaders.length, - className: "h-24 text-center", - children: - "\uD45C\uC2DC\uD560 \uC774\uC288\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", - }), - }), - }), - ], - }), - }), - ], - }), - ], - }), - ], - }); -} diff --git a/viewer/effort_js/src/services/error.js b/viewer/effort_js/src/services/error.js deleted file mode 100644 index 998a956..0000000 --- a/viewer/effort_js/src/services/error.js +++ /dev/null @@ -1,15 +0,0 @@ -// src/services/error.ts -/** - * API 요청 실패 시 공통으로 사용할 에러 처리 함수 - * @param message 프로덕션 환경에서 보여줄 기본 에러 메시지 - * @param response fetch API의 응답 객체 - */ -export const handleApiError = async (message, response) => { - if (import.meta.env.DEV) { - const errorBody = await response.text(); - throw new Error( - `[Dev] ${message} | URL: ${response.url} | Status: ${response.status} ${response.statusText} | Body: ${errorBody || "Empty"}`, - ); - } - throw new Error(message); -}; diff --git a/viewer/effort_js/src/services/feedback.js b/viewer/effort_js/src/services/feedback.js deleted file mode 100644 index 0c52058..0000000 --- a/viewer/effort_js/src/services/feedback.js +++ /dev/null @@ -1,128 +0,0 @@ -// src/services/feedback.ts -import { handleApiError } from "../../../src/services/error"; -// --- API 함수 --- -const getFeedbacksSearchApiUrl = (projectId, channelId) => - `/api/v2/projects/${projectId}/channels/${channelId}/feedbacks/search`; -const getFeedbackFieldsApiUrl = (projectId, channelId) => - `/api/projects/${projectId}/channels/${channelId}/fields`; -const getIssuesApiUrl = (projectId) => - `/api/projects/${projectId}/issues/search`; -/** - * 특정 채널의 피드백 목록을 조회합니다. - */ -export const getFeedbacks = async (projectId, channelId) => { - const url = getFeedbacksSearchApiUrl(projectId, channelId); - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }); - if (!response.ok) { - await handleApiError("피드백 목록을 불러오는 데 실패했습니다.", response); - } - const result = await response.json(); - return result.items || []; -}; -/** - * 특정 채널의 동적 폼 필드 스키마를 조회합니다. - */ -export const getFeedbackFields = async (projectId, channelId) => { - const url = getFeedbackFieldsApiUrl(projectId, channelId); - const response = await fetch(url); - if (!response.ok) { - await handleApiError( - "피드백 필드 정보를 불러오는 데 실패했습니다.", - response, - ); - } - const apiFields = await response.json(); - if (!Array.isArray(apiFields)) { - console.error("Error: Fields API response is not an array.", apiFields); - return []; - } - return apiFields - .filter((field) => field.status === "ACTIVE") - .map((field) => ({ - id: field.key, - name: field.name, - type: field.format, - })); -}; -/** - * 특정 채널에 새로운 피드백을 생성합니다. - */ -export const createFeedback = async (projectId, channelId, feedbackData) => { - const url = `/api/projects/${projectId}/channels/${channelId}/feedbacks`; - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(feedbackData), - }); - if (!response.ok) { - await handleApiError("피드백 생성에 실패했습니다.", response); - } - return response.json(); -}; -/** - * 프로젝트의 이슈를 검색합니다. - */ -export const searchIssues = async (projectId, query) => { - const url = getIssuesApiUrl(projectId); - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: { name: query }, - limit: 10, - page: 1, - sort: { createdAt: "ASC" }, - }), - }); - if (!response.ok) { - await handleApiError("이슈 검색에 실패했습니다.", response); - } - const result = await response.json(); - return result.items || []; -}; -/** - * 특정 ID의 피드백 상세 정보를 조회합니다. - */ -export const getFeedbackById = async (projectId, channelId, feedbackId) => { - const url = `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`; - const response = await fetch(url); - if (!response.ok) { - await handleApiError( - "피드백 상세 정보를 불러오는 데 실패했습니다.", - response, - ); - } - return response.json(); -}; -/** - * 특정 피드백을 수정합니다. - */ -export const updateFeedback = async ( - projectId, - channelId, - feedbackId, - feedbackData, -) => { - const url = `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`; - const response = await fetch(url, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(feedbackData), - }); - if (!response.ok) { - await handleApiError("피드백 수정에 실패했습니다.", response); - } - return response.json(); -}; diff --git a/viewer/effort_js/src/services/issue.js b/viewer/effort_js/src/services/issue.js deleted file mode 100644 index 0ac14e7..0000000 --- a/viewer/effort_js/src/services/issue.js +++ /dev/null @@ -1,24 +0,0 @@ -// src/services/issue.ts -import { handleApiError } from "../../../src/services/error"; -/** - * 특정 프로젝트의 모든 이슈를 검색합니다. - * @param projectId 프로젝트 ID - * @returns 이슈 목록 Promise - */ -export const getIssues = async (projectId) => { - const url = `/api/projects/${projectId}/issues/search`; - // body를 비워서 보내면 모든 이슈를 가져오는 것으로 가정합니다. - // 실제 API 명세에 따라 수정이 필요할 수 있습니다. - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }); - if (!response.ok) { - await handleApiError("이슈 목록을 불러오는 데 실패했습니다.", response); - } - const result = await response.json(); - return result.items || []; -}; diff --git a/viewer/effort_js/src/services/project.js b/viewer/effort_js/src/services/project.js deleted file mode 100644 index 93884ca..0000000 --- a/viewer/effort_js/src/services/project.js +++ /dev/null @@ -1,36 +0,0 @@ -const API_BASE_URL = "/api"; -/** - * 모든 접근 가능한 프로젝트 목록을 가져옵니다. - * 현재는 ID가 1인 프로젝트 하나만 가져오도록 구현되어 있습니다. - */ -export const getProjects = async () => { - try { - const project = await getProjectById("1"); - return project ? [project] : []; - } catch (error) { - console.error("Failed to fetch projects:", error); - return []; - } -}; -/** - * 특정 ID를 가진 프로젝트의 상세 정보를 가져옵니다. - * @param id - 조회할 프로젝트의 ID - */ -export const getProjectById = async (id) => { - try { - // 'project' -> 'projects'로 수정 - const response = await fetch(`${API_BASE_URL}/projects/${id}`); - if (!response.ok) { - throw new Error(`API call failed with status: ${response.status}`); - } - const data = await response.json(); - // API 응답(id: number)을 내부 모델(id: string)로 변환 - return { - ...data, - id: data.id.toString(), - }; - } catch (error) { - console.error(`Failed to fetch project with id ${id}:`, error); - return undefined; - } -}; diff --git a/viewer/effort_js/src/vite.config.js b/viewer/effort_js/src/vite.config.js deleted file mode 100644 index 86215eb..0000000 --- a/viewer/effort_js/src/vite.config.js +++ /dev/null @@ -1,36 +0,0 @@ -import path from "node:path"; -import react from "@vitejs/plugin-react"; -import { defineConfig, loadEnv } from "vite"; -import tailwindcss from "tailwindcss"; -import autoprefixer from "autoprefixer"; -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ""); - return { - plugins: [react()], - css: { - postcss: { - plugins: [tailwindcss, autoprefixer], - }, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, - server: { - proxy: { - // 나머지 /api 경로 처리 - "/api": { - target: env.VITE_API_PROXY_TARGET, - changeOrigin: true, - configure: (proxy, _options) => { - proxy.on("proxyReq", (proxyReq, _req, _res) => { - proxyReq.setHeader("X-Api-Key", env.VITE_API_KEY); - proxyReq.removeHeader("cookie"); - }); - }, - }, - }, - }, - }; -}); diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index 33b64e7..ab9a304 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -4,7 +4,7 @@ import { MainLayout } from "@/components/MainLayout"; import { FeedbackCreatePage } from "@/pages/FeedbackCreatePage"; import { FeedbackListPage } from "@/pages/FeedbackListPage"; import { FeedbackDetailPage } from "@/pages/FeedbackDetailPage"; -import { IssueViewerPage } from "@/pages/IssueViewerPage"; +import { IssueListPage } from "@/pages/IssueListPage"; function App() { const defaultProjectId = import.meta.env.VITE_DEFAULT_PROJECT_ID || "1"; @@ -37,8 +37,8 @@ function App() { element={} /> - {/* 채널 비종속 페이지 */} - } /> + {/* 채널 비종속 페��지 */} + } /> {/* 여기에 이슈 상세 페이지 라우트 추가 예정 */} diff --git a/viewer/src/components/DynamicForm.tsx b/viewer/src/components/DynamicForm.tsx index 097ab16..f7b9be6 100644 --- a/viewer/src/components/DynamicForm.tsx +++ b/viewer/src/components/DynamicForm.tsx @@ -27,7 +27,8 @@ export function DynamicForm({ initialData = EMPTY_INITIAL_DATA, // 기본값으로 상수 사용 submitButtonText = "제출", }: DynamicFormProps) { - const [formData, setFormData] = useState>(initialData); + const [formData, setFormData] = + useState>(initialData); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { diff --git a/viewer/src/components/DynamicTable.tsx b/viewer/src/components/DynamicTable.tsx index 6ebbf96..0d28204 100644 --- a/viewer/src/components/DynamicTable.tsx +++ b/viewer/src/components/DynamicTable.tsx @@ -9,6 +9,7 @@ import { type ColumnDef, type ColumnFiltersState, type ExpandedState, + type Row, type SortingState, type VisibilityState, } from "@tanstack/react-table"; @@ -24,7 +25,7 @@ import { } from "lucide-react"; import { useMemo, useState, Fragment } from "react"; import type { DateRange } from "react-day-picker"; -import { Link, useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -56,32 +57,34 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import type { Feedback, FeedbackField, Issue } from "@/services/feedback"; import { cn } from "@/lib/utils"; -interface DynamicTableProps { - columns: FeedbackField[]; - data: Feedback[]; - projectId: string; - channelId: string; +// --- 공용 타입 정의 --- +interface BaseData { + id: string | number; + updatedAt: string; + [key: string]: unknown; } -const DEFAULT_COLUMN_ORDER = [ - "id", - "title", - "contents", - "issues", - "customer", - "updatedAt", -]; +interface FieldSchema { + id: string; + name: string; + type: string; +} -export function DynamicTable({ +interface DynamicTableProps { + columns: FieldSchema[]; + data: TData[]; + onRowClick: (row: TData) => void; + renderExpandedRow?: (row: Row) => React.ReactNode; +} + +export function DynamicTable({ columns: rawColumns, data, - projectId, - channelId, -}: DynamicTableProps) { - const navigate = useNavigate(); + onRowClick, + renderExpandedRow, +}: DynamicTableProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); @@ -89,109 +92,99 @@ export function DynamicTable({ const [globalFilter, setGlobalFilter] = useState(""); const [date, setDate] = useState(); - const columns = useMemo[]>(() => { - const orderedRawColumns = [...rawColumns].sort((a, b) => { - const indexA = DEFAULT_COLUMN_ORDER.indexOf(a.id); - const indexB = DEFAULT_COLUMN_ORDER.indexOf(b.id); - if (indexA === -1 && indexB === -1) return 0; - if (indexA === -1) return 1; - if (indexB === -1) return -1; - return indexA - indexB; - }); - - const generatedColumns: ColumnDef[] = orderedRawColumns.map( - (field) => ({ - accessorKey: field.id, - header: ({ column }) => { - if (field.id === "issues") { - return {field.name}; - } - return ( - - column.toggleSorting(column.getIsSorted() === "asc") - } - > - {field.name} - - - ); - }, - cell: ({ row }) => { - const value = row.original[field.id]; - switch (field.id) { - case "issues": { - const issues = value as Issue[] | undefined; - if (!issues || issues.length === 0) return "N/A"; - return ( - - {issues.map((issue) => ( - e.stopPropagation()} - > - {issue.name} - - ))} - - ); - } - case "title": - return ( - - {String(value ?? "N/A")} - - ); - case "contents": { - const content = String(value ?? "N/A"); - const truncated = - content.length > 50 - ? `${content.substring(0, 50)}...` - : content; - return ( - - {truncated} - - ); - } - case "createdAt": - case "updatedAt": - return String(value ?? "N/A").substring(0, 10); - default: - if (typeof value === "object" && value !== null) { - return JSON.stringify(value); - } - return String(value ?? "N/A"); - } - }, - }), - ); - - return [ - { - id: "expander", - header: () => null, - cell: ({ row }) => { - return ( - { - e.stopPropagation(); - row.toggleExpanded(); - }} - > - {row.getIsExpanded() ? "▼" : "▶"} - - ); - }, + const columns = useMemo[]>(() => { + const generatedColumns: ColumnDef[] = rawColumns.map((field) => ({ + accessorKey: field.id, + header: ({ column }) => { + if (field.id === "issues") { + return {field.name}; + } + return ( + column.toggleSorting(column.getIsSorted() === "asc")} + > + {field.name} + + + ); }, - ...generatedColumns, - ]; - }, [rawColumns]); + cell: ({ row }) => { + const value = row.original[field.id]; + switch (field.id) { + case "issues": { + const issues = value as { id: string; name: string }[] | undefined; + if (!issues || issues.length === 0) return "N/A"; + return ( + + {issues.map((issue) => ( + e.stopPropagation()} + > + {issue.name} + + ))} + + ); + } + case "name": + case "title": + return ( + + {String(value ?? "N/A")} + + ); + case "contents": + case "description": { + const content = String(value ?? "N/A"); + const truncated = + content.length > 50 ? `${content.substring(0, 50)}...` : content; + return ( + + {truncated} + + ); + } + case "createdAt": + case "updatedAt": + return String(value ?? "N/A").substring(0, 10); + default: + if (typeof value === "object" && value !== null) { + return JSON.stringify(value); + } + return String(value ?? "N/A"); + } + }, + })); + + if (renderExpandedRow) { + return [ + { + id: "expander", + header: () => null, + cell: ({ row }) => { + return ( + { + e.stopPropagation(); + row.toggleExpanded(); + }} + > + {row.getIsExpanded() ? "▼" : "▶"} + + ); + }, + }, + ...generatedColumns, + ]; + } + return generatedColumns; + }, [rawColumns, renderExpandedRow]); const filteredData = useMemo(() => { if (!date?.from) { @@ -233,12 +226,6 @@ export function DynamicTable({ onGlobalFilterChange: setGlobalFilter, }); - const handleRowClick = (feedbackId: string) => { - navigate( - `/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, - ); - }; - return ( @@ -341,7 +328,7 @@ export function DynamicTable({ handleRowClick(row.original.id.toString())} + onClick={() => onRowClick(row.original)} className="cursor-pointer hover:bg-muted/50" > {row.getVisibleCells().map((cell) => ( @@ -353,17 +340,10 @@ export function DynamicTable({ ))} - {row.getIsExpanded() && ( + {row.getIsExpanded() && renderExpandedRow && ( - - - - {row.original.title} - - - {row.original.contents} - - + + {renderExpandedRow(row)} )} @@ -372,7 +352,7 @@ export function DynamicTable({ ) : ( 표시할 데이터가 없습니다. @@ -457,5 +437,3 @@ export function DynamicTable({ ); } - -export default DynamicTable; diff --git a/viewer/src/components/ui/calendar.tsx b/viewer/src/components/ui/calendar.tsx index 0b596da..bae9024 100644 --- a/viewer/src/components/ui/calendar.tsx +++ b/viewer/src/components/ui/calendar.tsx @@ -4,7 +4,11 @@ import { ChevronLeftIcon, ChevronRightIcon, } from "lucide-react"; -import { type DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; +import { + type DayButton, + DayPicker, + getDefaultClassNames, +} from "react-day-picker"; import { cn } from "@/lib/utils"; import { Button, buttonVariants } from "@/components/ui/button"; diff --git a/viewer/src/pages/FeedbackDetailPage.tsx b/viewer/src/pages/FeedbackDetailPage.tsx index 3e00cf9..eafd7a0 100644 --- a/viewer/src/pages/FeedbackDetailPage.tsx +++ b/viewer/src/pages/FeedbackDetailPage.tsx @@ -2,17 +2,9 @@ import { useState, useEffect, useMemo } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { DynamicForm } from "@/components/DynamicForm"; import { useSyncChannelId } from "@/hooks/useSyncChannelId"; -import { - getFeedbackById, - updateFeedback, -} from "@/services/feedback"; +import { getFeedbackById, updateFeedback } from "@/services/feedback"; import { ErrorDisplay } from "@/components/ErrorDisplay"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; export function FeedbackDetailPage() { diff --git a/viewer/src/pages/FeedbackListPage.tsx b/viewer/src/pages/FeedbackListPage.tsx index b53239a..d1f38bd 100644 --- a/viewer/src/pages/FeedbackListPage.tsx +++ b/viewer/src/pages/FeedbackListPage.tsx @@ -11,6 +11,7 @@ import { } from "@/services/feedback"; import { ErrorDisplay } from "@/components/ErrorDisplay"; import { Button } from "@/components/ui/button"; +import type { Row } from "@tanstack/react-table"; export function FeedbackListPage() { useSyncChannelId(); // URL의 channelId를 전역 상태와 동기화 @@ -53,6 +54,13 @@ export function FeedbackListPage() { ); }; + const renderExpandedRow = (row: Row) => ( + + {row.original.title} + {row.original.contents} + + ); + if (loading) { return 로딩 중...; } @@ -72,6 +80,7 @@ export function FeedbackListPage() { columns={schema} data={feedbacks} onRowClick={handleRowClick} + renderExpandedRow={renderExpandedRow} /> )} diff --git a/viewer/src/pages/IssueListPage.tsx b/viewer/src/pages/IssueListPage.tsx new file mode 100644 index 0000000..fd5b1c5 --- /dev/null +++ b/viewer/src/pages/IssueListPage.tsx @@ -0,0 +1,83 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useSettingsStore } from "@/store/useSettingsStore"; +import { DynamicTable } from "@/components/DynamicTable"; +import { + getIssues, + getIssueFields, + type Issue, + type IssueField, +} from "@/services/issue"; +import { ErrorDisplay } from "@/components/ErrorDisplay"; +import type { Row } from "@tanstack/react-table"; + +export function IssueListPage() { + const { projectId } = useSettingsStore(); + const _navigate = useNavigate(); + + const [schema, setSchema] = useState(null); + const [issues, setIssues] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!projectId) return; + + const fetchSchemaAndIssues = async () => { + try { + setLoading(true); + setError(null); + + const schemaData = await getIssueFields(); + setSchema(schemaData); + + const issuesData = await getIssues(projectId); + setIssues(issuesData); + } catch (err) { + setError( + err instanceof Error ? err.message : "데이터 로딩에 실패했습니다.", + ); + } finally { + setLoading(false); + } + }; + + fetchSchemaAndIssues(); + }, [projectId]); + + const handleRowClick = (row: Issue) => { + // 상세 페이지 구현 시 주석 해제 + // navigate(`/projects/${projectId}/issues/${row.id}`); + console.log("Clicked issue:", row); + }; + + const renderExpandedRow = (row: Row) => ( + + {row.original.name} + {row.original.description} + + ); + + if (loading) { + return 로딩 중...; + } + + return ( + + + 이슈 목록 + + {error && } + {schema && ( + + + + )} + + ); +} diff --git a/viewer/src/pages/IssueViewerPage.d.ts b/viewer/src/pages/IssueViewerPage.d.ts deleted file mode 100644 index 5b75486..0000000 --- a/viewer/src/pages/IssueViewerPage.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare function IssueViewerPage(): import("react/jsx-runtime").JSX.Element; diff --git a/viewer/src/pages/IssueViewerPage.tsx b/viewer/src/pages/IssueViewerPage.tsx deleted file mode 100644 index 5c5e594..0000000 --- a/viewer/src/pages/IssueViewerPage.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// src/pages/IssueViewerPage.tsx -import { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; -import { getIssues, type Issue } from "@/services/issue"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { ErrorDisplay } from "@/components/ErrorDisplay"; - -// 테이블 헤더 정의 -const issueTableHeaders = [ - { key: "title", label: "Title" }, - { key: "feedbackCount", label: "Feedback Count" }, - { key: "description", label: "Description" }, - { key: "status", label: "Status" }, - { key: "createdAt", label: "Created" }, - { key: "updatedAt", label: "Updated" }, - { key: "category", label: "Category" }, -]; - -export function IssueViewerPage() { - const { projectId } = useParams<{ projectId: string }>(); - const [issues, setIssues] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (!projectId) return; - - setLoading(true); - setError(null); - getIssues(projectId) - .then(setIssues) - .catch((err) => setError((err as Error).message)) - .finally(() => setLoading(false)); - }, [projectId]); - - if (error) { - return ; - } - - return ( - - - 이슈 목록 - - - {loading && 로딩 중...} - {!loading && ( - - - - - {issueTableHeaders.map((header) => ( - {header.label} - ))} - - - - {issues.length > 0 ? ( - issues.map((issue) => ( - - {issueTableHeaders.map((header) => ( - - {String(issue[header.key] ?? "")} - - ))} - - )) - ) : ( - - - 표시할 이슈가 없습니다. - - - )} - - - - )} - - - ); -} diff --git a/viewer/src/services/issue.ts b/viewer/src/services/issue.ts index 5ed43ad..e7f8471 100644 --- a/viewer/src/services/issue.ts +++ b/viewer/src/services/issue.ts @@ -1,34 +1,49 @@ // src/services/issue.ts import { handleApiError } from "./error"; -// API 응답에 대한 타입을 정의합니다. -// 실제 API 명세에 따라 더 구체적으로 작성해야 합니다. export interface Issue { id: string; - title: string; - feedbackCount: number; + name: string; description: string; status: string; + category: string; createdAt: string; updatedAt: string; - category: string; - [key: string]: unknown; // 그 외 다른 필드들 + + feedbackCount: number; + [key: string]: unknown; +} + +export interface IssueField { + id: string; + name: string; + type: "text" | "textarea" | "number" | "select"; + readOnly?: boolean; } +/** + * 이슈 목록에 표시할 필드 스키마를 반환합니다. + * 순서: Status, ID, Name, Description, Category + */ +export const getIssueFields = async (): Promise => { + const fields: IssueField[] = [ + { id: "status", name: "Status", type: "text" }, + { id: "id", name: "ID", type: "text" }, + { id: "name", name: "Name", type: "text" }, + { id: "description", name: "Description", type: "textarea" }, + { id: "category", name: "Category", type: "text" }, + ]; + return Promise.resolve(fields); +}; + /** * 특정 프로젝트의 모든 이슈를 검색합니다. - * @param projectId 프로젝트 ID - * @returns 이슈 목록 Promise */ export const getIssues = async (projectId: string): Promise => { const url = `/api/projects/${projectId}/issues/search`; - // body를 비워서 보내면 모든 이슈를 가져오는 것으로 가정합니다. - // 실제 API 명세에 따라 수정이 필요할 수 있습니다. const response = await fetch(url, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); @@ -37,14 +52,12 @@ export const getIssues = async (projectId: string): Promise => { } const result = await response.json(); + // API 응답을 그대로 사용합니다. return result.items || []; }; /** * 특정 프로젝트의 단일 이슈 상세 정보를 가져옵니다. - * @param projectId 프로젝트 ID - * @param issueId 이슈 ID - * @returns 이슈 상세 정보 Promise */ export const getIssue = async ( projectId: string, @@ -60,5 +73,6 @@ export const getIssue = async ( ); } + // API 응답을 그대로 사용합니다. return response.json(); }; diff --git a/viewer/src/store/useSettingsStore.ts b/viewer/src/store/useSettingsStore.ts index b09bf16..6b63333 100644 --- a/viewer/src/store/useSettingsStore.ts +++ b/viewer/src/store/useSettingsStore.ts @@ -25,4 +25,3 @@ export const useSettingsStore = create()( }, ), ); - diff --git a/viewer/tsconfig.json b/viewer/tsconfig.json index bf9832c..a51e218 100644 --- a/viewer/tsconfig.json +++ b/viewer/tsconfig.json @@ -26,5 +26,11 @@ "@/*": ["./src/*"] } }, - "include": ["src", "effort_js/src/services/error.js", "effort_js/src/services/feedback.js", "effort_js/src/services/issue.js", "effort_js/src/services/project.js"] + "include": [ + "src", + "effort_js/src/services/error.js", + "effort_js/src/services/feedback.js", + "effort_js/src/services/issue.js", + "effort_js/src/services/project.js" + ] }
- {row.original.contents} -
{row.original.contents}
{row.original.description}
로딩 중...