+
+
피드백 작성
아래 폼을 작성하여 피드백을 제출해주세요.
-
-
- {error &&
}
- {submitMessage && (
-
- {submitMessage}
-
- )}
-
+ {schema && (
+
+ )}
+ {submitMessage && (
+
+ {submitMessage}
+
+ )}
);
}
\ No newline at end of file
diff --git a/viewer/src/pages/FeedbackDetailPage.tsx b/viewer/src/pages/FeedbackDetailPage.tsx
index c8c57ad..c6f1f4d 100644
--- a/viewer/src/pages/FeedbackDetailPage.tsx
+++ b/viewer/src/pages/FeedbackDetailPage.tsx
@@ -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;
diff --git a/viewer/src/pages/FeedbackListPage.tsx b/viewer/src/pages/FeedbackListPage.tsx
index b347289..fe68610 100644
--- a/viewer/src/pages/FeedbackListPage.tsx
+++ b/viewer/src/pages/FeedbackListPage.tsx
@@ -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
([]);
+ useSyncChannelId(); // URL의 channelId를 전역 상태와 동기화
+ const { projectId, channelId } = useSettingsStore();
+ const navigate = useNavigate();
+
+ const [schema, setSchema] = useState(null);
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 () => {
+ 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 로딩 중...
;
+ return 로딩 중...
;
}
return (
-
-
+
+
피드백 목록
{error &&
}
-
+ {schema && (
+
+ )}
);
}
diff --git a/viewer/src/pages/IssueViewerPage.tsx b/viewer/src/pages/IssueViewerPage.tsx
index ba06817..26fbe36 100644
--- a/viewer/src/pages/IssueViewerPage.tsx
+++ b/viewer/src/pages/IssueViewerPage.tsx
@@ -41,60 +41,53 @@ export function IssueViewerPage() {
.finally(() => setLoading(false));
}, [projectId]);
+ if (error) {
+ return
;
+ }
+
return (
-
-
- 이슈 뷰어
-
- 프로젝트: {projectId}
-
-
-
- {error &&
}
-
-
-
- 이슈 목록
-
-
- {loading && 로딩 중...
}
- {!loading && (
-
-
-
-
- {issueTableHeaders.map((header) => (
- {header.label}
- ))}
-
-
-
- {issues.length > 0 ? (
- issues.map((issue) => (
-
- {issueTableHeaders.map((header) => (
-
- {String(issue[header.key] ?? "")}
-
- ))}
-
- ))
- ) : (
-
-
- 표시할 이슈가 없습니다.
-
+
+
+ 이슈 목록
+
+
+ {loading && 로딩 중...
}
+ {!loading && (
+
+
+
+
+ {issueTableHeaders.map((header) => (
+ {header.label}
+ ))}
+
+
+
+ {issues.length > 0 ? (
+ issues.map((issue) => (
+
+ {issueTableHeaders.map((header) => (
+
+ {String(issue[header.key] ?? "")}
+
+ ))}
- )}
-
-
-
- )}
-
-
-
+ ))
+ ) : (
+
+
+ 표시할 이슈가 없습니다.
+
+
+ )}
+
+
+
+ )}
+
+
);
}
\ No newline at end of file
diff --git a/viewer/src/services/issue.ts b/viewer/src/services/issue.ts
index 1616083..1e03f2f 100644
--- a/viewer/src/services/issue.ts
+++ b/viewer/src/services/issue.ts
@@ -38,4 +38,24 @@ export const getIssues = async (projectId: string): Promise
=> {
const result = await response.json();
return result.items || [];
+};
+
+/**
+ * 특정 프로젝트의 단일 이슈 상세 정보를 가져옵니다.
+ * @param projectId 프로젝트 ID
+ * @param issueId 이슈 ID
+ * @returns 이슈 상세 정보 Promise
+ */
+export const getIssue = async (
+ projectId: string,
+ issueId: string,
+): Promise => {
+ const url = `/api/projects/${projectId}/issues/${issueId}`;
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ await handleApiError("이슈 상세 정보를 불러오는 데 실패했습니다.", response);
+ }
+
+ return response.json();
};
\ No newline at end of file
diff --git a/viewer/src/store/useSettingsStore.ts b/viewer/src/store/useSettingsStore.ts
index 26bc2c4..0dad4de 100644
--- a/viewer/src/store/useSettingsStore.ts
+++ b/viewer/src/store/useSettingsStore.ts
@@ -5,17 +5,21 @@ type Theme = "light" | "dark" | "system";
interface SettingsState {
projectId: string | null;
+ channelId: string | null;
theme: Theme;
setProjectId: (projectId: string) => void;
+ setChannelId: (channelId: string) => void;
setTheme: (theme: Theme) => void;
}
export const useSettingsStore = create()(
persist(
(set) => ({
- projectId: "1", // 기본 프로젝트 ID를 1로 설정
- theme: "system",
+ projectId: import.meta.env.VITE_DEFAULT_PROJECT_ID,
+ channelId: import.meta.env.VITE_DEFAULT_CHANNEL_ID,
+ theme: "light",
setProjectId: (projectId) => set({ projectId }),
+ setChannelId: (channelId) => set({ channelId }),
setTheme: (theme) => set({ theme }),
}),
{
diff --git a/viewer/tailwind.config.ts b/viewer/tailwind.config.ts
index 21d744d..a1ef33d 100644
--- a/viewer/tailwind.config.ts
+++ b/viewer/tailwind.config.ts
@@ -19,38 +19,38 @@ const config: Config = {
},
extend: {
colors: {
- border: "oklch(var(--border))",
- input: "oklch(var(--input))",
- ring: "oklch(var(--ring))",
- background: "oklch(var(--background))",
- foreground: "oklch(var(--foreground))",
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
primary: {
- DEFAULT: "oklch(var(--primary))",
- foreground: "oklch(var(--primary-foreground))",
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
},
secondary: {
- DEFAULT: "oklch(var(--secondary))",
- foreground: "oklch(var(--secondary-foreground))",
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
- DEFAULT: "oklch(var(--destructive))",
- foreground: "oklch(var(--destructive-foreground))",
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
},
muted: {
- DEFAULT: "oklch(var(--muted))",
- foreground: "oklch(var(--muted-foreground))",
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
},
accent: {
- DEFAULT: "oklch(var(--accent))",
- foreground: "oklch(var(--accent-foreground))",
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
},
popover: {
- DEFAULT: "oklch(var(--popover))",
- foreground: "oklch(var(--popover-foreground))",
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
},
card: {
- DEFAULT: "oklch(var(--card))",
- foreground: "oklch(var(--card-foreground))",
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
diff --git a/viewer/vite.config.ts b/viewer/vite.config.ts
index d514a6d..53b82de 100644
--- a/viewer/vite.config.ts
+++ b/viewer/vite.config.ts
@@ -30,9 +30,9 @@ export default defineConfig(({ mode }) => {
proxyReq.setHeader("X-Api-Key", env.VITE_API_KEY);
proxyReq.removeHeader("cookie");
});
- }
- }
- }
- }
+ },
+ },
+ },
+ },
};
});
\ No newline at end of file