From d799b6f57cb5203c7342c52e5812020220b5a378 Mon Sep 17 00:00:00 2001 From: nelee Date: Tue, 12 May 2026 13:24:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20UI=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20=EB=B0=8F=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProjectCard: type 필드 라벨 + status 기반 dot, 마감일만 표시 - ProjectBoardFilter: pill → 탭 언더라인, 최근 순 텍스트 토글 - ProjectBoardHeader: 헤딩 간소화 (프로젝트 목록 → 프로젝트) - ProjectCreationModal: 생성 후 자동 이동 → step 4 확인 화면으로 변경, DialogTitle 추가 - ProjectBoardProvider: 기본 정렬 asc → desc (최근 순) - tasks.ts: handleApiError plain object 직렬화 개선 (JSON.stringify) - projectMemos: project_memos 테이블 생성 후 API 복구 Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/projectMemos/route.ts | 33 ++----- src/app/api/tasks/tasks.ts | 6 +- .../project/ui/ProjectBoardFilter.tsx | 97 +++++-------------- .../project/ui/ProjectBoardHeader.tsx | 9 +- src/features/project/ui/ProjectCard.tsx | 96 ++++++++---------- .../project/ui/ProjectCreationModal.tsx | 88 +++++++++++------ src/providers/ProjectBoardProvider.tsx | 4 +- 7 files changed, 142 insertions(+), 191 deletions(-) diff --git a/src/app/api/projectMemos/route.ts b/src/app/api/projectMemos/route.ts index c25ceae..87b3b9f 100644 --- a/src/app/api/projectMemos/route.ts +++ b/src/app/api/projectMemos/route.ts @@ -108,7 +108,6 @@ export async function GET(request: Request) { const offset = (page - 1) * limit; const orderDirection = sortBy === "newest" ? "desc" : "asc"; - // user_id만 가져오기 (JOIN은 나중에) const { data: memos, error: fetchError, @@ -130,10 +129,9 @@ export async function GET(request: Request) { ); } - // user_id들을 수집해서 한 번에 조회 const userIds = [...new Set(memos?.map((m) => m.user_id) || [])]; - const userMap: Record = {}; + const userMap: Record = {}; if (userIds.length > 0) { const { data: users } = await supabase .from("users") @@ -145,7 +143,6 @@ export async function GET(request: Request) { }); } - // memos에 author 정보 추가 const memosWithAuthor = memos?.map((memo) => ({ ...memo, author: userMap[memo.user_id] || { @@ -168,6 +165,7 @@ export async function GET(request: Request) { return errorResponse(error, "메모 조회에 실패했습니다"); } } + /** * POST /api/projectMemos * 메모 생성 @@ -185,7 +183,6 @@ export async function POST(request: Request) { ); } - // 한국 시간(KST, UTC+9)으로 저장 const now = new Date(); const kstTime = new Date(now.getTime() + 9 * 60 * 60 * 1000); @@ -198,8 +195,8 @@ export async function POST(request: Request) { content: content.trim(), created_at: kstTime.toISOString(), updated_at: kstTime.toISOString(), - is_pinned: false, // 새 메모는 기본적으로 고정되지 않음 - pinned_at: null, // 고정되지 않으므로 null + is_pinned: false, + pinned_at: null, }, ]) .select() @@ -207,7 +204,6 @@ export async function POST(request: Request) { if (error) throw error; - // 작성자 정보 추가 const { data: author } = await supabase .from("users") .select("user_id, user_name, email") @@ -301,10 +297,7 @@ export async function DELETE(request: Request) { if (updateError) throw updateError; return Response.json( - { - message: "메모가 삭제되었습니다", - memo_id: memoId, - }, + { message: "메모가 삭제되었습니다", memo_id: memoId }, { status: 200 } ); } catch (error) { @@ -323,8 +316,6 @@ export async function PATCH(request: Request) { const body = await request.json(); const { is_pinned } = body; - // console.log("🔧 서버 수신 데이터:", { memoId, body, is_pinned }); - if (!memoId) { return Response.json({ error: "메모 ID가 필수입니다" }, { status: 400 }); } @@ -336,22 +327,16 @@ export async function PATCH(request: Request) { ); } - const updateData = { - is_pinned, - pinned_at: is_pinned ? new Date().toISOString() : null, - }; - - // console.log("💾 DB 업데이트 데이터:", updateData); - const { data, error } = await supabase .from("project_memos") - .update(updateData) + .update({ + is_pinned, + pinned_at: is_pinned ? new Date().toISOString() : null, + }) .eq("memo_id", memoId) .select() .single(); - // console.log("📊 DB 업데이트 결과:", { data, error }); - if (error) throw error; return Response.json(data, { status: 200 }); diff --git a/src/app/api/tasks/tasks.ts b/src/app/api/tasks/tasks.ts index 836a34d..11fc3f1 100644 --- a/src/app/api/tasks/tasks.ts +++ b/src/app/api/tasks/tasks.ts @@ -51,8 +51,10 @@ function sanitizeTaskData>( * 에러 핸들링 헬퍼 */ function handleApiError(operation: string, error: unknown): ApiResponse { - const err = error instanceof Error ? error : new Error(String(error)); - console.error(`${operation} 실패:`, err); + const err = error instanceof Error + ? error + : new Error(typeof error === "object" ? JSON.stringify(error) : String(error)); + console.error(`${operation} 실패:`, error); return { data: null, error: err }; } diff --git a/src/features/project/ui/ProjectBoardFilter.tsx b/src/features/project/ui/ProjectBoardFilter.tsx index e11b586..1833e46 100644 --- a/src/features/project/ui/ProjectBoardFilter.tsx +++ b/src/features/project/ui/ProjectBoardFilter.tsx @@ -1,94 +1,45 @@ "use client"; import { useProjectBoard } from "@/providers/ProjectBoardProvider"; -import { ArrowUpNarrowWide, ArrowDownWideNarrow, RotateCcw } from "lucide-react"; +import { ChevronDown } from "lucide-react"; import { cn } from "@/lib/utils/utils"; const VIEW_OPTIONS = [ - { value: "all", label: "전체" }, + { value: "all", label: "전체 프로젝트" }, { value: "personal", label: "내 프로젝트" }, ]; -const DATE_OPTIONS = [ - { value: "startedAt", label: "시작일" }, - { value: "endedAt", label: "마감일" }, - { value: "createdAt", label: "생성일" }, - { value: "updatedAt", label: "수정일" }, -]; - -const DEFAULT_FILTER = { view: "all", date: "startedAt", sort: "asc" }; - -const pill = (active: boolean) => - cn( - "px-2.5 py-1 rounded-md text-sm transition-colors", - active - ? "bg-main-500/10 dark:bg-main-400/10 text-main-600 dark:text-main-300 font-medium" - : "text-gray-500 dark:text-gray-400 hover:text-foreground hover:bg-muted/40" - ); - -const Divider = () => ; - export default function ProjectBoardFilter() { const { filter, setFilter } = useProjectBoard(); - const isNonDefault = - filter.view !== DEFAULT_FILTER.view || - filter.date !== DEFAULT_FILTER.date || - filter.sort !== DEFAULT_FILTER.sort; + const sortLabel = filter.sort === "desc" ? "최근 순" : "오래된 순"; return ( -
- {VIEW_OPTIONS.map((opt) => ( - - ))} - - - - {DATE_OPTIONS.map((opt) => ( - - ))} - - +
+
+ {VIEW_OPTIONS.map((opt) => ( + + ))} +
- - - {isNonDefault && ( - <> - - - - )}
); } diff --git a/src/features/project/ui/ProjectBoardHeader.tsx b/src/features/project/ui/ProjectBoardHeader.tsx index 0f69016..6314862 100644 --- a/src/features/project/ui/ProjectBoardHeader.tsx +++ b/src/features/project/ui/ProjectBoardHeader.tsx @@ -9,13 +9,8 @@ export default function ProjectBoardHeader() { return ( <> -
-
-

프로젝트 목록

-

- Taskry에서 프로젝트를 생성하고 관리합니다. -

-
+
+

프로젝트

diff --git a/src/features/project/ui/ProjectCard.tsx b/src/features/project/ui/ProjectCard.tsx index e0943c3..c865707 100644 --- a/src/features/project/ui/ProjectCard.tsx +++ b/src/features/project/ui/ProjectCard.tsx @@ -8,38 +8,26 @@ import { showToast } from "@/lib/utils/toast"; import { useQueryClient } from "@tanstack/react-query"; import { queryKeys } from "@/lib/constants/queryKeys"; import type { Project, ProjectStatus } from "../model"; -import { Pencil, CalendarDays } from "lucide-react"; +import { Pencil } from "lucide-react"; interface ProjectCardProps { project: Project; projectMember: Record | null; } -const STATUS_CONFIG: Record = { - active: { label: "진행중", dot: "bg-emerald-400", text: "text-emerald-600 dark:text-emerald-400" }, - completed: { label: "완료", dot: "bg-gray-300 dark:bg-gray-500", text: "text-gray-400 dark:text-gray-500" }, - archived: { label: "일시정지", dot: "bg-amber-400", text: "text-amber-500 dark:text-amber-400" }, +const STATUS_DOT: Record = { + active: "bg-main-500", + completed: "bg-gray-300 dark:bg-gray-500", + archived: "bg-amber-400", }; -function formatDate(d: string) { - const date = new Date(d); - const dateStr = date.toLocaleDateString("ko-KR", { +function formatDeadline(d?: string) { + if (!d) return null; + return new Date(d).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", - }); - const timeStr = date.toLocaleTimeString("ko-KR", { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); - return `${dateStr.replace(/\. /g, ".").replace(/\.$/, "")} ${timeStr}`; -} - -function formatDateRange(start?: string, end?: string) { - if (!start && !end) return null; - if (!end) return formatDate(start!); - return `${formatDate(start!)} – ${formatDate(end)}`; + }).replace(/\. /g, ".").replace(/\.$/, "") + " 마감"; } export default function ProjectCard({ project, projectMember }: ProjectCardProps) { @@ -47,8 +35,8 @@ export default function ProjectCard({ project, projectMember }: ProjectCardProps const queryClient = useQueryClient(); const memberCount = projectMember?.[project.project_id] ?? 1; - const status = STATUS_CONFIG[project.status] ?? STATUS_CONFIG.active; - const dateRange = formatDateRange(project.started_at, project.ended_at); + const dot = STATUS_DOT[project.status] ?? STATUS_DOT.active; + const deadline = formatDeadline(project.ended_at); async function handleDelete() { await deleteProject(project.project_id); @@ -60,52 +48,48 @@ export default function ProjectCard({ project, projectMember }: ProjectCardProps return (
router.push(`/project/workspace/${project.project_id}`)} - className="group relative flex flex-col bg-card rounded-2xl border border-border p-5 cursor-pointer transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 hover:border-main-200 dark:hover:border-main-700" + className="group relative flex flex-col bg-white dark:bg-card rounded-[14px] border border-[#bde3ec] dark:border-border p-6 cursor-pointer transition-all duration-200 hover:shadow-md hover:-translate-y-0.5" > - {/* 상태 */} -
- - {status.label} + {/* 타입 라벨 */} +
+ + {project.type || "기타"}
{/* 제목 + 설명 */} -
-

+
+

{project.project_name}

-

+

{project.description || "설명이 없습니다."}

- {/* 날짜 */} - {dateRange && ( -
- - {dateRange} -
- )} - - {/* 팀원 수 + 호버 액션 */} -
- - - {memberCount}명 + {/* 마감일 + 멤버 수 + 호버 액션 */} +
+ + {deadline ?? "마감일 없음"} - {/* 호버 시 나타나는 액션 버튼 */} -
e.stopPropagation()} - > - -
- + +
+ +
diff --git a/src/features/project/ui/ProjectCreationModal.tsx b/src/features/project/ui/ProjectCreationModal.tsx index 2575dd2..92f5a61 100644 --- a/src/features/project/ui/ProjectCreationModal.tsx +++ b/src/features/project/ui/ProjectCreationModal.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import * as Collapsible from "@radix-ui/react-collapsible"; -import { X, ChevronDown, ChevronUp, Briefcase, User, BookOpen, Building2 } from "lucide-react"; +import { X, ChevronDown, ChevronUp, Briefcase, User, BookOpen, Building2, CheckCircle2 } from "lucide-react"; import { createProject, updateProjectMember } from "../model"; import { showToast } from "@/lib/utils/toast"; import { useQueryClient } from "@tanstack/react-query"; @@ -57,6 +57,7 @@ export function ProjectCreationModal({ open, onOpenChange }: ProjectCreationModa const [selectedUser, setSelectedUser] = useState(null); const [members, setMembers] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); + const [createdProjectId, setCreatedProjectId] = useState(null); const { data: userList = [] } = useQuery({ queryKey: queryKeys.users.all, @@ -86,6 +87,7 @@ export function ProjectCreationModal({ open, onOpenChange }: ProjectCreationModa setIsExpanded(false); setSelectedUser(null); setMembers([]); + setCreatedProjectId(null); }; const handleClose = () => { @@ -148,10 +150,12 @@ export function ProjectCreationModal({ open, onOpenChange }: ProjectCreationModa } queryClient.invalidateQueries({ queryKey: queryKeys.projects.all }); - showToast("프로젝트가 생성되었습니다.", "success"); - handleClose(); if (projectId) { - router.push(`/project/workspace/${projectId}`); + setCreatedProjectId(projectId); + setStep(4); + } else { + showToast("프로젝트가 생성되었습니다.", "success"); + handleClose(); } } catch { showToast("생성에 실패했습니다.", "error"); @@ -167,6 +171,7 @@ export function ProjectCreationModal({ open, onOpenChange }: ProjectCreationModa + 프로젝트 생성 {/* 헤더 */}
@@ -174,11 +179,11 @@ export function ProjectCreationModal({ open, onOpenChange }: ProjectCreationModa {[1, 2, 3].map((s) => (
))}
-

Step {step} / 3

+ {step < 4 &&

Step {step} / 3

}
)} + {step === 4 && ( +
+ +
+

프로젝트가 생성되었습니다!

+

칸반보드로 바로 이동할까요?

+
+
+ )} + {step === 3 && (
@@ -345,28 +360,47 @@ export function ProjectCreationModal({ open, onOpenChange }: ProjectCreationModa {/* 푸터 */}
- {step > 1 && ( - - )} -
- {step < 3 ? ( - + {step === 4 ? ( + <> + + + ) : ( - + <> + {step > 1 && ( + + )} +
+ {step < 3 ? ( + + ) : ( + + )} + )}
diff --git a/src/providers/ProjectBoardProvider.tsx b/src/providers/ProjectBoardProvider.tsx index 4ab6174..9fc1cbd 100644 --- a/src/providers/ProjectBoardProvider.tsx +++ b/src/providers/ProjectBoardProvider.tsx @@ -17,8 +17,8 @@ const ProjectBoardContext = createContext(u export function ProjectBoardProvider({ children }: { children: ReactNode }) { const [filter, setFilter] = useState({ view: "all", - date: "startedAt", - sort: "asc", + date: "createdAt", + sort: "desc", }); return (