diff --git a/src/features/team/components/real-team-rules-panel.tsx b/src/features/team/components/real-team-rules-panel.tsx index 51c2ff8..9bda9c8 100644 --- a/src/features/team/components/real-team-rules-panel.tsx +++ b/src/features/team/components/real-team-rules-panel.tsx @@ -1,5 +1,18 @@ -import { LoaderCircle, RefreshCw, RotateCcw, Save } from "lucide-react"; -import { type FormEvent, useEffect, useState } from "react"; +import { + LoaderCircle, + PencilLine, + RefreshCw, + RotateCcw, + Save, + X, +} from "lucide-react"; +import { + type FormEvent, + type ReactNode, + useEffect, + useMemo, + useState, +} from "react"; import { AppPanel, AppPanelHeader } from "@/components/app-shell"; import { Badge } from "@/components/ui/badge"; @@ -13,6 +26,12 @@ import { useTeamRuleQuery, useUpdateTeamRuleMutation, } from "@/features/team/hooks/use-team-space-queries"; +import { + type TeamRuleMarkdownBlock, + type TeamRuleMarkdownListItem, + parseTeamRuleInlineCode, + parseTeamRuleMarkdown, +} from "@/features/team/lib/team-rule-markdown"; import { ApiError, getApiErrorMessage } from "@/lib/api/client"; import type { MyProjectGroup } from "@/lib/types/project-group"; import type { TeamRuleResponse } from "@/lib/types/team-space"; @@ -32,7 +51,12 @@ export function RealTeamRulesPanel({ const [draftContent, setDraftContent] = useState(""); const [syncedRuleKey, setSyncedRuleKey] = useState(null); const [feedback, setFeedback] = useState(null); + const [isEditing, setIsEditing] = useState(false); const teamRuleKey = teamRule ? getTeamRuleKey(teamRule) : null; + const markdownBlocks = useMemo( + () => parseTeamRuleMarkdown(teamRule?.content ?? ""), + [teamRule?.content], + ); const isBlank = draftContent.trim().length === 0; const isTooLong = draftContent.length > maxTeamRuleContentLength; const isDirty = Boolean(teamRule && draftContent !== teamRule.content); @@ -59,7 +83,28 @@ export function RealTeamRulesPanel({ if (result.data) { setDraftContent(result.data.content); setSyncedRuleKey(getTeamRuleKey(result.data)); + setIsEditing(false); + } + } + + function handleStartEdit() { + if (!teamRule) { + return; + } + + setDraftContent(teamRule.content); + setFeedback(null); + setIsEditing(true); + } + + function handleCancelEdit() { + if (!teamRule) { + return; } + + setDraftContent(teamRule.content); + setFeedback(null); + setIsEditing(false); } function handleResetDraft() { @@ -106,6 +151,7 @@ export function RealTeamRulesPanel({ message: "팀 룰을 저장했어요.", tone: "success", }); + setIsEditing(false); } catch (error: unknown) { setFeedback({ message: getTeamRuleSaveErrorMessage(error), @@ -180,108 +226,252 @@ export function RealTeamRulesPanel({ - {getTeamRuleStatusLabel({ - isDirty, - isFetching: teamRuleQuery.isFetching, - isSaving: updateTeamRuleMutation.isPending, - })} - +
+ + {getTeamRuleStatusLabel({ + isDirty: isEditing && isDirty, + isFetching: teamRuleQuery.isFetching, + isSaving: updateTeamRuleMutation.isPending, + })} + + {isEditing ? null : ( + + )} +
} description="브랜치, 커밋, 리뷰, 공유 방식을 Markdown으로 정리해요." eyebrow="Rulebook" title="팀 룰" /> -
- -
-
-
- - - {draftContent.length.toLocaleString()} /{" "} - {maxTeamRuleContentLength.toLocaleString()} - + {isEditing ? ( + + +
+
+
+ + + {draftContent.length.toLocaleString()} /{" "} + {maxTeamRuleContentLength.toLocaleString()} + +
+