내비게이션, 테마 적용
This commit is contained in:
@@ -1,106 +1,98 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSettingsStore } from "@/store/useSettingsStore";
|
||||
import { useSyncChannelId } from "@/hooks/useSyncChannelId";
|
||||
import { DynamicForm } from "@/components/DynamicForm";
|
||||
import { getFeedbackFields, createFeedback } from "@/services/feedback";
|
||||
import type { FeedbackField, CreateFeedbackRequest } from "@/services/feedback";
|
||||
import {
|
||||
getFeedbackSchema,
|
||||
createFeedback,
|
||||
type FeedbackSchema,
|
||||
type CreateFeedbackRequest,
|
||||
} from "@/services/feedback";
|
||||
import { ErrorDisplay } from "@/components/ErrorDisplay";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export function FeedbackCreatePage() {
|
||||
useSyncChannelId();
|
||||
const navigate = useNavigate();
|
||||
const [fields, setFields] = useState<FeedbackField[]>([]);
|
||||
const { projectId, channelId } = useSettingsStore();
|
||||
|
||||
const [schema, setSchema] = useState<FeedbackSchema | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitMessage, setSubmitMessage] = useState<string | null>(null);
|
||||
|
||||
// TODO: projectId와 channelId는 URL 파라미터나 컨텍스트에서 가져와야 합니다.
|
||||
const projectId = "1";
|
||||
const channelId = "4";
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFields = async () => {
|
||||
if (!projectId || !channelId) return;
|
||||
|
||||
const fetchSchema = 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" as const };
|
||||
}
|
||||
return field;
|
||||
});
|
||||
|
||||
setFields(processedFields);
|
||||
const schemaData = await getFeedbackSchema(projectId, channelId);
|
||||
setSchema(schemaData);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError("알 수 없는 오류가 발생했습니다.");
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "폼을 불러오는 데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFields();
|
||||
fetchSchema();
|
||||
}, [projectId, channelId]);
|
||||
|
||||
const handleSubmit = async (formData: Record<string, any>) => {
|
||||
if (!projectId || !channelId) return;
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
setSubmitMessage(null);
|
||||
|
||||
const requestData: CreateFeedbackRequest = {
|
||||
...formData,
|
||||
issueNames: [],
|
||||
issueNames: [], // 이슈 이름은 현재 UI에서 받지 않으므로 빈 배열로 설정
|
||||
};
|
||||
|
||||
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;
|
||||
setError(err instanceof Error ? err.message : "피드백 등록 중 오류가 발생했습니다.");
|
||||
throw err; // DynamicForm이 오류 상태를 인지하도록 re-throw
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div>폼을 불러오는 중...</div>;
|
||||
return <div className="text-center py-10">폼을 불러오는 중...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorDisplay message={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="space-y-2 mb-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold">피드백 작성</h1>
|
||||
<p className="text-muted-foreground">
|
||||
아래 폼을 작성하여 피드백을 제출해주세요.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="mt-6">
|
||||
<DynamicForm fields={fields} onSubmit={handleSubmit} />
|
||||
{error && <ErrorDisplay message={error} />}
|
||||
{submitMessage && (
|
||||
<div className="mt-4 p-3 bg-green-100 text-green-800 rounded-md">
|
||||
{submitMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{schema && (
|
||||
<DynamicForm
|
||||
schema={schema}
|
||||
onSubmit={handleSubmit}
|
||||
submitButtonText="제출하기"
|
||||
/>
|
||||
)}
|
||||
{submitMessage && (
|
||||
<div className="mt-4 p-3 bg-green-100 text-green-800 rounded-md">
|
||||
{submitMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { DynamicForm } from "@/components/DynamicForm";
|
||||
import { useSyncChannelId } from "@/hooks/useSyncChannelId";
|
||||
import {
|
||||
getFeedbackFields,
|
||||
getFeedbackSchema,
|
||||
getFeedbackById,
|
||||
updateFeedback,
|
||||
type Feedback,
|
||||
type FeedbackSchema,
|
||||
} from "@/services/feedback";
|
||||
import type { Feedback, FeedbackField } from "@/services/feedback";
|
||||
import { ErrorDisplay } from "@/components/ErrorDisplay";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export function FeedbackDetailPage() {
|
||||
useSyncChannelId();
|
||||
const { projectId, channelId, feedbackId } = useParams<{
|
||||
projectId: string;
|
||||
channelId: string;
|
||||
|
||||
@@ -1,68 +1,74 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useSettingsStore } from "@/store/useSettingsStore";
|
||||
import { useSyncChannelId } from "@/hooks/useSyncChannelId";
|
||||
import { DynamicTable } from "@/components/DynamicTable";
|
||||
import { getFeedbacks, getFeedbackFields } from "@/services/feedback";
|
||||
import type { Feedback, FeedbackField } from "@/services/feedback";
|
||||
import {
|
||||
getFeedbacks,
|
||||
getFeedbackSchema,
|
||||
type Feedback,
|
||||
type FeedbackSchema,
|
||||
} from "@/services/feedback";
|
||||
import { ErrorDisplay } from "@/components/ErrorDisplay";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function FeedbackListPage() {
|
||||
const [fields, setFields] = useState<FeedbackField[]>([]);
|
||||
useSyncChannelId(); // URL의 channelId를 전역 상태와 동기화
|
||||
const { projectId, channelId } = useSettingsStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [schema, setSchema] = useState<FeedbackSchema | null>(null);
|
||||
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// TODO: projectId와 channelId는 URL 파라미터나 컨텍스트에서 가져와야 합니다.
|
||||
const projectId = "1";
|
||||
const channelId = "4";
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFieldsAndFeedbacks = async () => {
|
||||
if (!projectId || !channelId) return;
|
||||
|
||||
const fetchSchemaAndFeedbacks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const fieldsData = await getFeedbackFields(projectId, channelId);
|
||||
setFields(fieldsData);
|
||||
setError(null);
|
||||
|
||||
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("테이블 구조를 불러오는 데 실패했습니다.");
|
||||
}
|
||||
const schemaData = await getFeedbackSchema(projectId, channelId);
|
||||
setSchema(schemaData);
|
||||
|
||||
const feedbacksData = await getFeedbacks(projectId, channelId);
|
||||
setFeedbacks(feedbacksData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "데이터 로딩에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFieldsAndFeedbacks();
|
||||
fetchSchemaAndFeedbacks();
|
||||
}, [projectId, channelId]);
|
||||
|
||||
const handleRowClick = (row: Feedback) => {
|
||||
navigate(`/projects/${projectId}/channels/${channelId}/feedbacks/${row.id}`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div>로딩 중...</div>;
|
||||
return <div className="text-center py-10">로딩 중...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">피드백 목록</h1>
|
||||
<Button asChild>
|
||||
<Link to="new">새 피드백 작성</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{error && <ErrorDisplay message={error} />}
|
||||
<DynamicTable
|
||||
columns={fields}
|
||||
data={feedbacks}
|
||||
projectId={projectId}
|
||||
channelId={channelId}
|
||||
/>
|
||||
{schema && (
|
||||
<DynamicTable
|
||||
schema={schema}
|
||||
data={feedbacks}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,60 +41,53 @@ export function IssueViewerPage() {
|
||||
.finally(() => setLoading(false));
|
||||
}, [projectId]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorDisplay message={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 md:p-8">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">이슈 뷰어</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
프로젝트: {projectId}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{error && <ErrorDisplay message={error} />}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>이슈 목록</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && <p className="text-center">로딩 중...</p>}
|
||||
{!loading && (
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{issueTableHeaders.map((header) => (
|
||||
<TableHead key={header.key}>{header.label}</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{issues.length > 0 ? (
|
||||
issues.map((issue) => (
|
||||
<TableRow key={issue.id}>
|
||||
{issueTableHeaders.map((header) => (
|
||||
<TableCell key={`${issue.id}-${header.key}`}>
|
||||
{String(issue[header.key] ?? "")}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={issueTableHeaders.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
표시할 이슈가 없습니다.
|
||||
</TableCell>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>이슈 목록</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && <p className="text-center">로딩 중...</p>}
|
||||
{!loading && (
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{issueTableHeaders.map((header) => (
|
||||
<TableHead key={header.key}>{header.label}</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{issues.length > 0 ? (
|
||||
issues.map((issue) => (
|
||||
<TableRow key={issue.id}>
|
||||
{issueTableHeaders.map((header) => (
|
||||
<TableCell key={`${issue.id}-${header.key}`}>
|
||||
{String(issue[header.key] ?? "")}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={issueTableHeaders.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
표시할 이슈가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user