e.stopPropagation()}>
+
) => e.stopPropagation()}>
handleDeleteProject(project.projectId)}
+ onClick={() => handleDeleteProject(project.project_id)}
/>
diff --git a/src/components/features/project/ProjectDateCard.tsx b/src/features/project/ui/ProjectDateCard.tsx
similarity index 100%
rename from src/components/features/project/ProjectDateCard.tsx
rename to src/features/project/ui/ProjectDateCard.tsx
diff --git a/src/components/features/project/ProjectForm.tsx b/src/features/project/ui/ProjectForm.tsx
similarity index 73%
rename from src/components/features/project/ProjectForm.tsx
rename to src/features/project/ui/ProjectForm.tsx
index 2d4ffe0..c1c3019 100644
--- a/src/components/features/project/ProjectForm.tsx
+++ b/src/features/project/ui/ProjectForm.tsx
@@ -2,9 +2,9 @@
import Button from "@/components/ui/Button";
import { Icon } from "@/components/shared/Icon";
-import { Calendar22 } from "@/components/features/project/Calendar";
-import { StatusSelect } from "@/components/features/project/StatusSelect";
-import { TypeSelect } from "@/components/features/project/TypeSelect";
+import { Calendar22 } from "@/features/project/ui/Calendar";
+import { StatusSelect } from "@/features/project/ui/StatusSelect";
+import { TypeSelect } from "@/features/project/ui/TypeSelect";
import { Input } from "@/components/ui/shadcn/Input";
import { Label } from "@/components/ui/shadcn/Label";
import { Textarea } from "@/components/ui/shadcn/Textarea";
@@ -18,7 +18,9 @@ import {
import { showToast } from "@/lib/utils/toast";
import { getUser, getUserById } from "@/lib/api/users";
import { useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { queryKeys } from "@/lib/constants/queryKeys";
import { ComboBox, type Item } from "./ComboBox";
import ProjectDateCard from "./ProjectDateCard";
import { RoleSelect } from "./RoleSelect";
@@ -38,7 +40,9 @@ interface ProjectProps {
export default function ProjectForm() {
const router = useRouter();
- const [projectId, setProjectId] = useState
("");
+ const [projectId, setProjectId] = useState(
+ () => (typeof window !== "undefined" ? sessionStorage.getItem("current_Project_Id") ?? "" : "")
+ );
const [projectData, setProjectData] = useState({
projectName: "",
type: "",
@@ -51,91 +55,83 @@ export default function ProjectForm() {
description: "",
});
const [user, setUser] = useState- (null);
- const [userList, setUserList] = useState
- ([]);
- const [projectMember, setProjectMember] = useState([]);
-
- useEffect(() => {
- const storedProjectId = sessionStorage.getItem("current_Project_Id");
-
- if (storedProjectId) {
- setProjectId(storedProjectId);
- }
- }, [router]);
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- // 유저 조회
- const userResult = await getUser();
- if (userResult.data) {
- setUserList(
- userResult.data.map(({ user_id, user_name, email }) => ({
- id: user_id,
- label: `${user_name} (${email})`,
- value: user_name,
- email,
- }))
- );
- }
-
- if (!projectId) {
- return;
- }
-
- // 프로젝트 정보 및 멤버 조회
- const [projectResult, memberResult] = await Promise.all([
- getProjectById(projectId),
- getProjectMember(projectId),
- ]);
-
-
- // 프로젝트 데이터 설정
- const project = projectResult.data?.[0];
- if (project) {
- setProjectData({
- ...project,
- projectName: project.project_name,
- startedAt: project.started_at,
- endedAt: project.ended_at,
- createdAt: project.created_at,
- updatedAt: project.updated_at,
- techStack: project.tech_stack,
- });
- }
-
- // 프로젝트 멤버 데이터 설정
- if (memberResult.data) {
- const memberPromises = memberResult.data.map(async (member) => {
- const { data } = await getUserById("eq", member.user_id);
- const userInfo = data?.[0];
-
- if (!userInfo) {
- return null;
- }
+ const queryClient = useQueryClient();
+
+ interface ProjectMemberItem {
+ projectId: string;
+ userId: string;
+ userName: string;
+ email: string;
+ role: string;
+ }
+ const [projectMember, setProjectMember] = useState([]);
+
+ // 유저 목록 조회
+ const { data: userList = [] } = useQuery({
+ queryKey: queryKeys.users.all,
+ queryFn: async () => {
+ const result = await getUser();
+ return (result.data || []).map(({ user_id, user_name, email }) => ({
+ id: user_id,
+ label: `${user_name} (${email})`,
+ value: user_name,
+ email,
+ })) as Item[];
+ },
+ staleTime: 1000 * 60 * 10,
+ });
- return {
- projectId: projectId,
- userId: userInfo.user_id,
- userName: userInfo.user_name,
- email: userInfo.email,
- role: member.role,
- };
- });
+ // 프로젝트 정보 + 멤버 조회 (수정 모드)
+ useQuery({
+ queryKey: queryKeys.projectForm.detail(projectId),
+ queryFn: async () => {
+ if (!projectId) return null;
+
+ const [projectResult, memberResult] = await Promise.all([
+ getProjectById(projectId),
+ getProjectMember(projectId),
+ ]);
+
+ const project = projectResult.data?.[0];
+ if (project) {
+ setProjectData({
+ ...project,
+ projectName: project.project_name,
+ startedAt: project.started_at,
+ endedAt: project.ended_at,
+ createdAt: project.created_at,
+ updatedAt: project.updated_at,
+ techStack: project.tech_stack,
+ });
+ }
- const validMembers = (await Promise.all(memberPromises)).filter(
- Boolean
- );
- setProjectMember(validMembers);
- }
- } catch (err) {
- console.error(err);
+ if (memberResult.data) {
+ const memberPromises = memberResult.data.map(async (member) => {
+ const { data } = await getUserById("eq", member.user_id);
+ const userInfo = data?.[0];
+ if (!userInfo) return null;
+ return {
+ projectId,
+ userId: userInfo.user_id,
+ userName: userInfo.user_name,
+ email: userInfo.email,
+ role: member.role,
+ };
+ });
+ const validMembers = (await Promise.all(memberPromises)).filter(
+ (m): m is ProjectMemberItem => m !== null
+ );
+ setProjectMember(validMembers);
}
- };
- fetchData();
- }, [projectId]);
+
+ return null;
+ },
+ enabled: !!projectId,
+ staleTime: 1000 * 60 * 5,
+ });
// 일반 Input과 Textarea를 위한 handleChange
- const handleChange = (event: any) => {
+ const handleChange = (event: React.ChangeEvent) => {
const { name, value } = event.target;
setProjectData((prevProjectData) => ({
...prevProjectData,
@@ -213,10 +209,8 @@ export default function ProjectForm() {
setProjectMember(filterProjectMember);
};
- const handleSubmit = async (event: any) => {
- event.preventDefault();
-
- try {
+ const { mutate: submitProject, isPending: isSubmitting } = useMutation({
+ mutationFn: async () => {
let targetId = projectId;
if (!targetId) {
@@ -229,13 +223,21 @@ export default function ProjectForm() {
if (targetId) {
await updateProjectMember(targetId, projectMember);
}
-
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });
showToast("저장되었습니다.", "success");
router.push("/");
- } catch (error) {
+ },
+ onError: (error) => {
console.error(error);
showToast("저장에 실패했습니다.", "error");
- }
+ },
+ });
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ submitProject();
};
return (
@@ -384,9 +386,10 @@ export default function ProjectForm() {
variant="primary"
size={16}
className="hover:cursor-pointer mr-2 text-white"
- onClick={handleSubmit}
+ onClick={() => submitProject()}
+ disabled={isSubmitting}
>
- {projectId ? "수정 완료" : "프로젝트 생성"}
+ {isSubmitting ? "저장 중..." : projectId ? "수정 완료" : "프로젝트 생성"}