From 570e2c3fdab2f8e62ab532c4e5f02488688583e4 Mon Sep 17 00:00:00 2001 From: hwnagjokim Date: Wed, 17 Jun 2026 09:59:40 +0900 Subject: [PATCH 1/2] feat(team): add team name editing --- docs/team-space-api-spec.md | 1 + openapi/openapi.yaml | 34 ++++++++ .../hooks/use-project-group-queries.ts | 25 ++++++ .../components/real-team-manage-panel.tsx | 87 ++++++++++++++++--- .../team/components/team-space-view.tsx | 38 ++++++++ src/lib/api/mocks/handlers.ts | 58 ++++++++++++- src/lib/api/project-groups.ts | 11 +++ src/lib/types/project-group.ts | 5 ++ 8 files changed, 245 insertions(+), 14 deletions(-) diff --git a/docs/team-space-api-spec.md b/docs/team-space-api-spec.md index e19b66b..edce023 100644 --- a/docs/team-space-api-spec.md +++ b/docs/team-space-api-spec.md @@ -9,6 +9,7 @@ Base URL은 `/api`이며, 아래 경로는 모두 인증 토큰을 사용한다. | Method | Path | 용도 | | --- | --- | --- | | `GET` | `/project-groups/me` | 내 활성 팀 스페이스 조회 | +| `PATCH` | `/project-groups/{projectGroupId}/name` | 팀 스페이스 이름 수정 | | `PATCH` | `/project-groups/{projectGroupId}/admins/{targetUserId}` | 팀 스페이스 관리자 권한 부여 | | `DELETE` | `/project-groups/{projectGroupId}/admins/{targetUserId}` | 팀 스페이스 관리자 권한 회수 | | `GET` | `/project-groups/{projectGroupId}/checklists` | 체크리스트 목록 조회 | diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 9e7e332..efa75a7 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -492,6 +492,31 @@ paths: $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' + /project-groups/{projectGroupId}/name: + patch: + tags: [ProjectGroups] + security: + - bearerAuth: [] + summary: Update a project group's display name + parameters: + - $ref: '#/components/parameters/ProjectGroupId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProjectGroupNameRequest' + responses: + '200': + description: Project group name updated + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' /project-groups/{projectGroupId}/admins/{targetUserId}: patch: tags: [ProjectGroups] @@ -1462,6 +1487,15 @@ components: type: array items: $ref: '#/components/schemas/ProjectGroupMember' + UpdateProjectGroupNameRequest: + type: object + additionalProperties: false + required: [projectName] + properties: + projectName: + type: string + minLength: 1 + maxLength: 255 ProjectGroupMember: type: object additionalProperties: false diff --git a/src/features/project-groups/hooks/use-project-group-queries.ts b/src/features/project-groups/hooks/use-project-group-queries.ts index 8db4578..a674548 100644 --- a/src/features/project-groups/hooks/use-project-group-queries.ts +++ b/src/features/project-groups/hooks/use-project-group-queries.ts @@ -5,10 +5,13 @@ import { getMyProjectGroup, grantProjectGroupAdminPermission, revokeProjectGroupAdminPermission, + updateProjectGroupName, } from "@/lib/api/project-groups"; import type { + MyProjectGroup, ProjectGroupAdminPermissionRequest, ProjectGroupFinishRequest, + UpdateProjectGroupNameRequest, } from "@/lib/types/project-group"; import { isProjectGroupNotFoundError } from "@/features/project-groups/lib/errors"; @@ -64,6 +67,28 @@ export function useRevokeProjectGroupAdminPermissionMutation() { }); } +export function useUpdateProjectGroupNameMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: UpdateProjectGroupNameRequest) => + updateProjectGroupName(payload), + onSuccess: (_, variables) => { + queryClient.setQueryData( + projectGroupQueryKeys.me, + (current) => + current + ? { + ...current, + projectName: variables.projectName.trim(), + } + : current, + ); + queryClient.invalidateQueries({ queryKey: projectGroupQueryKeys.all }); + }, + }); +} + export function useFinishProjectGroupMutation() { const queryClient = useQueryClient(); diff --git a/src/features/team/components/real-team-manage-panel.tsx b/src/features/team/components/real-team-manage-panel.tsx index 8b7e77f..507a5fd 100644 --- a/src/features/team/components/real-team-manage-panel.tsx +++ b/src/features/team/components/real-team-manage-panel.tsx @@ -1,8 +1,10 @@ -import { CheckCircle2, LoaderCircle, ShieldCheck } from "lucide-react"; +import { CheckCircle2, LoaderCircle, Save, ShieldCheck } from "lucide-react"; +import { type FormEvent, useEffect, useState } from "react"; import { AppPanel, AppPanelHeader } from "@/components/app-shell"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { type ActionFeedback, RealActionFeedback, @@ -23,10 +25,13 @@ export function RealManagePanel({ hasCurrentUserAgreedFinish, isAdminPermissionPending, isFinishPending, + isProjectNamePending, onAdminPermissionChange, onFinishProjectGroup, + onProjectNameUpdate, pendingAdminPermissionTargetId, projectGroup, + projectNameFeedback, }: { canManageAdminPermissions: boolean; currentUserId: number; @@ -35,31 +40,86 @@ export function RealManagePanel({ hasCurrentUserAgreedFinish: boolean; isAdminPermissionPending: boolean; isFinishPending: boolean; + isProjectNamePending: boolean; onAdminPermissionChange: (member: ProjectGroupMember) => void; onFinishProjectGroup: () => void; + onProjectNameUpdate: (projectName: string) => void; pendingAdminPermissionTargetId: number | null; projectGroup: MyProjectGroup; + projectNameFeedback: ActionFeedback | null; }) { + const [projectNameInput, setProjectNameInput] = useState( + projectGroup.projectName, + ); + const trimmedProjectName = projectNameInput.trim(); + const isProjectNameChanged = trimmedProjectName !== projectGroup.projectName; + const isProjectNameInvalid = + trimmedProjectName.length === 0 || trimmedProjectName.length > 255; + const canSubmitProjectName = + canManageAdminPermissions && + isProjectNameChanged && + !isProjectNameInvalid && + !isProjectNamePending; + + useEffect(() => { + setProjectNameInput(projectGroup.projectName); + }, [projectGroup.projectName]); + + function handleProjectNameSubmit(event: FormEvent) { + event.preventDefault(); + + if (!canSubmitProjectName) { + return; + } + + onProjectNameUpdate(trimmedProjectName); + } + return (
active} - description="멤버 관리자 권한과 팀 종료 동의를 관리해요." + description="팀 이름, 멤버 관리자 권한, 팀 종료 동의를 관리해요." eyebrow="Manage" title="팀 관리" />
-
-