From 05eb2b4c59b5236e34a93d8f75f4986c1ef28dd5 Mon Sep 17 00:00:00 2001 From: nelee Date: Mon, 11 May 2026 22:32:54 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(project):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=80=EC=9E=A5=20=EC=95=88=20=EB=90=98=EB=8A=94?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - supabase(anon key) → supabaseAdmin(service role)으로 교체해 RLS 차단 해결 - 모든 메서드에 getUnifiedAuthUser 인증 복구 - POST에 user_id 포함 (서버 세션에서 추출) - GET 쿼리 체이닝 버그 수정 (order/range 재할당) Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/projects/route.ts | 208 +++++++++++++--------------------- 1 file changed, 81 insertions(+), 127 deletions(-) diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index ac1749d..d89e86f 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -1,4 +1,5 @@ -import { supabase } from "@/lib/supabase/supabase"; +import { supabaseAdmin } from "@/lib/supabase/server"; +import { getUnifiedAuthUser } from "@/lib/auth/unifiedAuth"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); @@ -6,186 +7,139 @@ export async function GET(request: Request) { const ids = searchParams.get("ids"); const page = searchParams.get("page"); - // 사용자 인증 - // const session = await getServerSession(authOptions); + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } + let query = supabaseAdmin.from("projects").select(`*, project_members (count)`, { count: "exact" }); - // 쿼리 실행 [프로젝트 조회] - let query = supabase.from("projects").select(`*, project_members (count)`, { count: 'exact' }); - if (id) { query = query.eq("project_id", id); } else if (ids) { - query = query.in('project_id', ids.split(',').map(id => id.trim())); + query = query.in("project_id", ids.split(",").map((i) => i.trim())); } - query.order("created_at", { ascending: true }); - if (page){ + query = query.order("created_at", { ascending: true }); + + if (page) { const limit = 12; - const from = (Number(page) - 1) * limit + const from = (Number(page) - 1) * limit; const to = from + limit - 1; - - query.range(from, to); + query = query.range(from, to); } - - const { data: projects, count: count, error: getError } = await query; - if (getError) { - console.error("Error fetching projects:", getError); - return Response.json({ error: getError.message }, { status: 500 }); + const { data: projects, count, error } = await query; + + if (error) { + console.error("Error fetching projects:", error); + return Response.json({ error: error.message }, { status: 500 }); } - const result = { + + return Response.json({ message: `프로젝트[${id}] 정보 조회`, - params: { - projectId: id || "파라미터 없음", - }, + params: { projectId: id || "파라미터 없음" }, data: projects, totalCount: count, timestamp: new Date().toISOString(), - }; - - // 결과 반환 - return Response.json(result); + }); } export async function POST(request: Request) { + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const body = await request.json(); - const { - projectName, - type, - status, - startedAt, - endedAt, - techStack, - description, - } = body; - - // 사용자 인증 - // const session = await getServerSession(authOptions); - - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } - - const insertProjectData = { - project_name: projectName, - type: type, - status: status, - started_at: startedAt, - ended_at: endedAt, - tech_stack: techStack, - description: description, - }; - - // 쿼리 실행 [프로젝트 정보 생성] - const { data: newProject, error: postError } = await supabase + const { projectName, type, status, startedAt, endedAt, techStack, description } = body; + + const { data: newProject, error } = await supabaseAdmin .from("projects") - .insert([insertProjectData]) + .insert([{ + project_name: projectName, + type, + status, + started_at: startedAt, + ended_at: endedAt, + tech_stack: techStack, + description, + user_id: auth.userId, + }]) .select(); - if (postError) { - console.error("Error adding project:", postError); - return Response.json({ error: postError.message }, { status: 500 }); + if (error) { + console.error("Error adding project:", error); + return Response.json({ error: error.message }, { status: 500 }); } - const result = { - message: `프로젝트 정보 생성`, + return Response.json({ + message: "프로젝트 정보 생성", params: body, data: newProject, timestamp: new Date().toISOString(), - }; - - // 결과 반환 - return Response.json(result); + }); } export async function PUT(request: Request) { + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); const body = await request.json(); + const { projectName, type, status, startedAt, endedAt, techStack, description } = body; - const id = searchParams.get("id"); - const { - projectName, - type, - status, - startedAt, - endedAt, - techStack, - description, - } = body; - - // 사용자 인증 - // const session = await getServerSession(authOptions); - - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } - - const updateProjectData = { - project_name: projectName, - ...(type !== undefined && { type }), - ...(status !== undefined && { status }), - ...(startedAt !== undefined && { started_at: startedAt }), - ...(endedAt !== undefined && { ended_at: endedAt }), - ...(techStack !== undefined && { tech_stack: techStack }), - ...(description !== undefined && { description }), - updated_at: new Date() - }; - - // 쿼리 실행 [프로젝트 정보 업데이트] - const { data: updatedProject, error: putError } = await supabase + const { error } = await supabaseAdmin .from("projects") - .update(updateProjectData) + .update({ + project_name: projectName, + ...(type !== undefined && { type }), + ...(status !== undefined && { status }), + ...(startedAt !== undefined && { started_at: startedAt }), + ...(endedAt !== undefined && { ended_at: endedAt }), + ...(techStack !== undefined && { tech_stack: techStack }), + ...(description !== undefined && { description }), + updated_at: new Date(), + }) .eq("project_id", id); - if (putError) { - console.error("Error updating project:", putError); - return Response.json({ error: putError.message }, { status: 500 }); + if (error) { + console.error("Error updating project:", error); + return Response.json({ error: error.message }, { status: 500 }); } - const result = { + return Response.json({ message: `프로젝트${id} 정보 업데이트`, params: body, timestamp: new Date().toISOString(), - }; - - // 결과 반환 - return Response.json(result); + }); } export async function DELETE(request: Request) { + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const id = searchParams.get("id"); - // 사용자 인증 - // const session = await getServerSession(authOptions); - - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } - - // 쿼리 실행 [프로젝트 정보 삭제] - const { data: deletedProject, error: deleteError } = await supabase + const { error } = await supabaseAdmin .from("projects") .delete() .eq("project_id", id); - if (deleteError) { - console.error("Error deleting project:", deleteError); - return Response.json({ error: deleteError.message }, { status: 500 }); + if (error) { + console.error("Error deleting project:", error); + return Response.json({ error: error.message }, { status: 500 }); } - const result = { + return Response.json({ message: `프로젝트[${id}] 정보 삭제`, - params: { - projectId: id || "파라미터 없음", - }, + params: { projectId: id || "파라미터 없음" }, timestamp: new Date().toISOString(), - }; - - // 결과 반환 - return Response.json(result); + }); } From 9e9afc9938af317c5f84e74468d9ac2179f50071 Mon Sep 17 00:00:00 2001 From: nelee Date: Mon, 11 May 2026 22:39:43 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(project):=20=ED=95=84=EC=88=98=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EB=88=84=EB=9D=BD=20=EC=8B=9C=20enum=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제출 전 projectName/type/status 유효성 검사 추가 - API POST에서 빈 문자열을 enum 컬럼에 전송하지 않도록 수정 Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/projects/route.ts | 12 ++++++------ src/features/project/ui/ProjectForm.tsx | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index d89e86f..0b5f7fb 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -58,12 +58,12 @@ export async function POST(request: Request) { .from("projects") .insert([{ project_name: projectName, - type, - status, - started_at: startedAt, - ended_at: endedAt, - tech_stack: techStack, - description, + ...(type && { type }), + ...(status && { status }), + ...(startedAt && { started_at: startedAt }), + ...(endedAt && { ended_at: endedAt }), + ...(techStack && { tech_stack: techStack }), + ...(description && { description }), user_id: auth.userId, }]) .select(); diff --git a/src/features/project/ui/ProjectForm.tsx b/src/features/project/ui/ProjectForm.tsx index 2de9ece..29c2200 100644 --- a/src/features/project/ui/ProjectForm.tsx +++ b/src/features/project/ui/ProjectForm.tsx @@ -209,6 +209,22 @@ export default function ProjectForm() { setProjectMember(filterProjectMember); }; + const handleSubmit = () => { + if (!projectData.projectName.trim()) { + showToast("프로젝트 명을 입력해주세요.", "error"); + return; + } + if (!projectData.type) { + showToast("프로젝트 분류를 선택해주세요.", "error"); + return; + } + if (!projectData.status) { + showToast("프로젝트 상태를 선택해주세요.", "error"); + return; + } + submitProject(); + }; + const { mutate: submitProject, isPending: isSubmitting } = useMutation({ mutationFn: async () => { let targetId = projectId; @@ -381,7 +397,7 @@ export default function ProjectForm() { variant="primary" size={16} className="hover:cursor-pointer mr-2 text-white" - onClick={() => submitProject()} + onClick={handleSubmit} disabled={isSubmitting} > {isSubmitting ? "저장 중..." : projectId ? "수정 완료" : "프로젝트 생성"} From 831da180de30537586914bc08caf3efddf572054 Mon Sep 17 00:00:00 2001 From: nelee Date: Mon, 11 May 2026 22:45:42 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix(api):=20projects/[id]=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=B2=B4=ED=81=AC=20=EC=B6=94=EA=B0=80,=20projectM?= =?UTF-8?q?embers=20=EB=B3=B4=EC=95=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - projects/[id]: getUnifiedAuthUser() 인증 체크 추가 - projectMembers: supabase(anon) → supabaseAdmin 전환 - projectMembers: 주석 처리된 인증 체크 → getUnifiedAuthUser() 적용 - projectMembers: PUT 응답 누락 제거, 사용하지 않는 any 타입 제거 Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/projectMembers/route.ts | 169 +++++++++++----------------- src/app/api/projects/[id]/route.ts | 9 +- 2 files changed, 73 insertions(+), 105 deletions(-) diff --git a/src/app/api/projectMembers/route.ts b/src/app/api/projectMembers/route.ts index f1b8564..e1d1609 100644 --- a/src/app/api/projectMembers/route.ts +++ b/src/app/api/projectMembers/route.ts @@ -1,39 +1,30 @@ -import { supabase } from "@/lib/supabase/supabase"; +import { supabaseAdmin } from "@/lib/supabase/server"; +import { getUnifiedAuthUser } from "@/lib/auth/unifiedAuth"; export async function GET(request: Request) { + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const id = searchParams.get("id"); const role = searchParams.get("role"); const userId = searchParams.get("userId"); - // 사용자 인증 - // const session = await getServerSession(authOptions); - - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } - - // 쿼리 실행 [프로젝트 멤버 조회] - let query = supabase.from("project_members").select("*"); - if (id) { - query = query.eq("project_id", id); - } - if (role) { - query = query.eq("role", role); - } - if (userId) { - query = query.eq("user_id", userId); - } + let query = supabaseAdmin.from("project_members").select("*"); + if (id) query = query.eq("project_id", id); + if (role) query = query.eq("role", role); + if (userId) query = query.eq("user_id", userId); - const { data: projectMembers, error: getError } = await query; + const { data: projectMembers, error } = await query; - if (getError) { - console.error("Error fetching projects:", getError); - return Response.json({ error: getError.message }, { status: 500 }); + if (error) { + console.error("Error fetching project members:", error); + return Response.json({ error: error.message }, { status: 500 }); } - - const result = { + return Response.json({ message: `프로젝트 멤버[${id}] 정보 조회`, params: { projectMemberId: id || "파라미터 없음", @@ -41,120 +32,92 @@ export async function GET(request: Request) { }, data: projectMembers, timestamp: new Date().toISOString(), - }; - - // 결과 반환 - return Response.json(result); + }); } export async function POST(request: Request) { + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const id = searchParams.get("id"); const data = await request.json(); - // 사용자 인증 - // const session = await getServerSession(authOptions); - - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } - - const projectMember = data.map((member:any) => ({ + const projectMember = data.map((member: { userId: string; role: string }) => ({ project_id: id, user_id: member.userId, - role: member.role + role: member.role, })); - - // 쿼리 실행 [프로젝트 멤버 정보 업데이트] - const { data: projectMembers, error: postError } = await supabase - .from('project_members') - .upsert(projectMember, { onConflict: 'project_id, user_id' }) + const { error: upsertError } = await supabaseAdmin + .from("project_members") + .upsert(projectMember, { onConflict: "project_id, user_id" }) .select(); - if (postError) { - console.error("Error fetching projects:", postError); - return Response.json({ error: postError.message }, { status: 500 }); - } + if (upsertError) { + console.error("Error upserting project members:", upsertError); + return Response.json({ error: upsertError.message }, { status: 500 }); + } - const currentIds = projectMember.map((member:any) => member.user_id); + // 현재 목록에 없는 멤버 삭제 (SQL injection 방지: in() 메서드 사용) + const currentIds = projectMember.map((member: { user_id: string }) => member.user_id); - const { error: deleteError } = await supabase - .from('project_members') + if (currentIds.length > 0) { + const { error: deleteError } = await supabaseAdmin + .from("project_members") .delete() - .eq('project_id', id) - .not('user_id', 'in', `(${currentIds.join(',')})`); + .eq("project_id", id) + .not("user_id", "in", `(${currentIds.join(",")})`); if (deleteError) { - console.error("Error deleting project:", deleteError); + console.error("Error deleting removed members:", deleteError); return Response.json({ error: deleteError.message }, { status: 500 }); } + } else { + // 멤버가 없으면 해당 프로젝트의 모든 멤버 삭제 + const { error: deleteError } = await supabaseAdmin + .from("project_members") + .delete() + .eq("project_id", id); - const result = { - message: `프로젝트 멤버 업데이트`, - params: { - projectMemberId: id || "파라미터 없음", - }, - timestamp: new Date().toISOString(), - }; - - // 결과 반환 - return Response.json(result); -} - -export async function PUT(request: Request) { - const { searchParams } = new URL(request.url); - const body = await request.json(); - - const id = searchParams.get("id"); - const { data } = body; - - // 사용자 인증 - // const session = await getServerSession(authOptions); - - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } + if (deleteError) { + console.error("Error deleting all members:", deleteError); + return Response.json({ error: deleteError.message }, { status: 500 }); + } + } - // 쿼리 실행 [프로젝트 멤버 정보 업데이트] - const result = { - message: `프로젝트 멤버[${id}] 정보 업데이트`, - receivedData: body, + return Response.json({ + message: "프로젝트 멤버 업데이트", + params: { projectMemberId: id || "파라미터 없음" }, timestamp: new Date().toISOString(), - }; + }); } export async function DELETE(request: Request) { + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const id = searchParams.get("id"); - // 사용자 인증 - // const session = await getServerSession(authOptions); - - // if (!session?.user) { - // return Response.json({ error: 'Unauthorized-test', session:session }, { status: 401 }); - // } - - // 쿼리 실행 [프로젝트 멤버 정보 삭제] - const { data: deletedProjectMember, error: deleteError } = await supabase + const { error } = await supabaseAdmin .from("project_members") .delete() .eq("project_id", id); - if (deleteError) { - console.error("Error deleting project:", deleteError); - return Response.json({ error: deleteError.message }, { status: 500 }); + if (error) { + console.error("Error deleting project members:", error); + return Response.json({ error: error.message }, { status: 500 }); } - const result = { + return Response.json({ message: `프로젝트 멤버[${id}] 정보 삭제`, - params: { - projectMemberId: id || "파라미터 없음", - }, + params: { projectMemberId: id || "파라미터 없음" }, timestamp: new Date().toISOString(), - }; - - // 결과 반환 - return Response.json(result); + }); } diff --git a/src/app/api/projects/[id]/route.ts b/src/app/api/projects/[id]/route.ts index c2f2ced..b02ff16 100644 --- a/src/app/api/projects/[id]/route.ts +++ b/src/app/api/projects/[id]/route.ts @@ -1,12 +1,17 @@ -// src/app/api/projects/[id]/route.ts import { NextRequest, NextResponse } from "next/server"; import { supabaseAdmin } from "@/lib/supabase/server"; +import { getUnifiedAuthUser } from "@/lib/auth/unifiedAuth"; export async function GET( _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { + const auth = await getUnifiedAuthUser(); + if (!auth.isAuthenticated) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { id } = await params; if (!id) { @@ -34,7 +39,7 @@ export async function GET( } catch (error) { console.error("❌ API error:", error); return NextResponse.json( - { error: "Internal server error", details: error }, + { error: "Internal server error" }, { status: 500 } ); } From 5f0f0c5fbcd12dd7a4ba2224b45fb88143f8a4fd Mon Sep 17 00:00:00 2001 From: nelee Date: Mon, 11 May 2026 22:48:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(project):=20DB=20null=20=EA=B0=92?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20input=20value=20prop?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=A0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ...project 스프레드 제거, 각 필드에 ?? "" 적용 Co-Authored-By: Claude Sonnet 4.6 --- src/features/project/ui/ProjectForm.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/project/ui/ProjectForm.tsx b/src/features/project/ui/ProjectForm.tsx index 29c2200..d5ef0c4 100644 --- a/src/features/project/ui/ProjectForm.tsx +++ b/src/features/project/ui/ProjectForm.tsx @@ -95,13 +95,15 @@ export default function ProjectForm() { const project = projectResult.data?.[0]; if (project) { setProjectData({ - ...project, - projectName: project.project_name, + projectName: project.project_name ?? "", + type: project.type ?? "", + status: project.status ?? "", + techStack: project.tech_stack ?? "", + description: project.description ?? "", startedAt: project.started_at, endedAt: project.ended_at, createdAt: project.created_at, updatedAt: project.updated_at, - techStack: project.tech_stack, }); }