Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .claude/agents/pr-writer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: pr-writer
description: "Use this agent when you need to create a pull request description for your current branch changes. This includes when you've completed a feature implementation, bug fix, or any code changes that need to be submitted for review. The agent analyzes your git diff and commits to generate a clean, professional PR following the team's template format.\\n\\nExamples:\\n\\n<example>\\nContext: User has finished implementing a new feature and wants to create a PR.\\nuser: \"PR 작성해줘\"\\nassistant: \"I'm going to use the Task tool to launch the pr-writer agent to analyze your branch changes and create a PR description.\"\\n<commentary>\\nSince the user wants to create a PR for their changes, use the pr-writer agent to analyze the branch diff and generate a well-structured PR description.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: User completed a bug fix and needs a PR.\\nuser: \"이 버그 수정 내용으로 PR 만들어줘\"\\nassistant: \"I'm going to use the Task tool to launch the pr-writer agent to create a PR for your bug fix.\"\\n<commentary>\\nThe user has completed a bug fix and needs a PR. Use the pr-writer agent to generate a PR with the fix: prefix and appropriate description.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: User wants to submit their refactoring work for review.\\nuser: \"리팩토링 작업 끝났어. PR 작성 부탁해\"\\nassistant: \"I'm going to use the Task tool to launch the pr-writer agent to summarize your refactoring changes into a clear PR.\"\\n<commentary>\\nThe user finished refactoring and needs a PR. Use the pr-writer agent to analyze the changes and create a chore: prefixed PR.\\n</commentary>\\n</example>"
model: sonnet
color: blue
---

You are a senior frontend developer with exceptional skills in writing clean, clear, and professional pull requests. You have deep experience with React, TypeScript, and modern frontend development practices, particularly in monorepo environments.

Your primary responsibility is to analyze the current branch's changes and create a well-structured PR that effectively communicates the changes to team members.

## Your Workflow

1. **Analyze Changes**: First, examine the current branch's git diff and commit history to understand what has changed.
- Run `git diff main...HEAD` or `git diff origin/main...HEAD` to see all changes
- Run `git log main..HEAD --oneline` to see commit messages
- Identify the files modified, added, or deleted

2. **Categorize the Change**: Determine the nature of the changes:
- `feat:` - New feature implementation
- `fix:` - Bug fixes
- `chore:` - Refactoring, configuration changes, minor updates, dependency updates
- `docs:` - Documentation changes
- `style:` - Code style/formatting changes (no logic changes)
- `refactor:` - Code refactoring without changing functionality

3. **Generate PR Content**: Create the PR following this exact template format:

```markdown
> ### [prefix]: [핵심 변경 사항을 간결하게 요약한 제목]
---

### 🏄🏼‍♂️‍ Summary (요약)

- [변경 사항의 핵심을 1-3문장으로 요약]

### 🫨 Describe your Change (변경사항)

- [구체적인 변경 내용을 bullet point로 나열]
- [파일/컴포넌트별 주요 변경 사항]
- [팀원들이 알아야 할 중요한 포인트]

### 🧐 Issue number and link (참고)

- [관련 이슈 번호나 링크, 없으면 "없음" 또는 적절한 내용]

### 📚 Reference (참조)

- [참조한 문서, 디자인, 또는 관련 PR 링크, 없으면 "없음"]
```

## Writing Guidelines

### Title
- Keep it concise but descriptive (under 50 characters if possible)
- Use Korean for the description part after the prefix
- Examples: `feat: 회고 작성 페이지 구현`, `fix: 로그인 토큰 만료 처리 버그 수정`, `chore: 불필요한 의존성 제거`

### Summary
- Write in Korean
- Focus on the "what" and "why"
- Be concise - team members should understand the PR's purpose in seconds

### Describe your Change
- List specific changes made
- Group related changes together
- Highlight any breaking changes or important considerations
- Mention any side effects or areas that might need attention
- If there are UI changes, describe what changed visually

### Issue and Reference
- Link to related issues if any exist in the commit messages or branch name
- Reference any design documents, Figma links, or related PRs
- If none exist, write "없음" or provide relevant context

## Quality Checklist

Before finalizing the PR, ensure:
- [ ] Title accurately reflects the main change
- [ ] Summary is clear and actionable
- [ ] All significant changes are documented
- [ ] Technical details are explained for complex changes
- [ ] No sensitive information is included
- [ ] Korean is used naturally and professionally

## Context Awareness

You are working in a Layer monorepo with:
- `apps/web/` - React web application
- `apps/mobile/` - React Native mobile app
- `packages/shared/` - Shared utilities

When describing changes, mention which part of the codebase is affected (웹, 모바일, 공통 패키지).

## Output Format

Always output the complete PR in markdown format, ready to be copied directly into GitHub/GitLab. Start with the title line and include all sections of the template.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,5 @@ dist
*.xcconfig

# Claude Code configuration
CLAUDE.md
CLAUDE.md
.claude//
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,21 @@ type MainQuestionsContentsProps = {
isDeleteMode: boolean;
handleDelete: (index: number) => void;
handleDragEnd: (result: any) => void;
handleContentChange: (index: number, newContent: string) => void;
};

export default function MainQuestionsContents({ questions, isDeleteMode, handleDelete, handleDragEnd }: MainQuestionsContentsProps) {
export default function MainQuestionsContents({ questions, isDeleteMode, handleDelete, handleDragEnd, handleContentChange }: MainQuestionsContentsProps) {
const { toast } = useToast();

const originalContentRef = useRef<{ [key: number]: string }>({});
const textareaRefs = useRef<{ [key: number]: HTMLTextAreaElement | null }>({});
const setRetroCreateData = useSetAtom(retrospectCreateAtom);

/**
* 질문 내용 변경 핸들러
*
* @param index - 질문 인덱스
* @param newContent - 새로운 질문 내용
* textarea 내용 변경 시 높이 자동 조절을 포함한 핸들러
*/
const handleContentChange = (index: number, newContent: string) => {
const updatedQuestions = questions.map((item, i) => (i === index ? { ...item, questionContent: newContent } : item));
setRetroCreateData((prev) => ({ ...prev, questions: updatedQuestions, isNewForm: true, formName: `커스텀 템플릿` }));
const handleTextareaChange = (index: number, newContent: string) => {
handleContentChange(index, newContent);

// textarea 높이 자동 조절
const textarea = textareaRefs.current[index];
Expand Down Expand Up @@ -146,7 +143,7 @@ export default function MainQuestionsContents({ questions, isDeleteMode, handleD
textareaRefs.current[index] = el;
}}
value={item.questionContent}
onChange={(e) => handleContentChange(index, e.target.value)}
onChange={(e) => handleTextareaChange(index, e.target.value)}
onFocus={() => handleContentFocus(index)}
onBlur={() => handleContentBlur(index)}
placeholder="질문을 입력해주세요"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Button, ButtonProvider } from "@/component/common/button";
import { Icon } from "@/component/common/Icon";
import { Spacing } from "@/component/common/Spacing";
Expand Down Expand Up @@ -29,7 +29,7 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp

const [isDeleteMode, setIsDeleteMode] = useState(false);
const [isAddMode, setIsAddMode] = useState(false);
const [backupQuestions, setBackupQuestions] = useState<Questions>([]);

const [retroCreateData, setRetroCreateData] = useAtom(retrospectCreateAtom);

// TODO: 아톰 구조 변경 (#593)
Expand All @@ -41,7 +41,12 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
const isInitializedCreateSpaceFlow = flow === "INFO";
const isInitializedProgressingCreateSpace = retrospectQuestions.length === 0;
const isInitializedCreateSpace = isInitializedCreateSpaceFlow || isInitializedProgressingCreateSpace;
const questions = isInitializedCreateSpace ? retroCreateData.questions : retrospectQuestions;
const originalQuestions = isInitializedCreateSpace ? retroCreateData.questions : retrospectQuestions;

// 수정 중인 질문들을 로컬 상태로 관리 (완료 버튼 클릭 시에만 atom에 반영)
const [editingQuestions, setEditingQuestions] = useState<Questions>(() => originalQuestions);
const [backupQuestions, setBackupQuestions] = useState<Questions>(editingQuestions);
const questions = editingQuestions;

/**
* 리스트의 아이템 순서 변경
Expand All @@ -68,11 +73,18 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
return;
}
const items = reorder(questions, result.source.index, result.destination.index);
if (isInitializedCreateSpace) {
setRetroCreateData((prev) => ({ ...prev, questions: items }));
} else {
setRetrospectQuestions(items);
}
setEditingQuestions(items);
};

/**
* 질문 내용 변경 핸들러
*
* @param index - 질문 인덱스
* @param newContent - 새로운 질문 내용
*/
const handleContentChange = (index: number, newContent: string) => {
const updatedQuestions = questions.map((item, i) => (i === index ? { ...item, questionContent: newContent } : item));
setEditingQuestions(updatedQuestions);
};

/**
Expand All @@ -82,12 +94,7 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
*/
const handleDelete = (index: number) => {
const updatedQuestions = questions.filter((_, i) => i !== index);
if (isInitializedCreateSpace) {
setRetroCreateData((prev) => ({ ...prev, questions: updatedQuestions }));
} else {
setRetrospectQuestions(updatedQuestions);
}

setEditingQuestions(updatedQuestions);
toast.success("삭제가 완료되었어요!");
};

Expand Down Expand Up @@ -118,11 +125,7 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
*/
const handleAddQuestionComplete = (content: string) => {
const newQuestions = [...questions, { questionType: "plain_text" as const, questionContent: content }];
if (isInitializedCreateSpace) {
setRetroCreateData((prev) => ({ ...prev, questions: newQuestions }));
} else {
setRetrospectQuestions(newQuestions);
}
setEditingQuestions(newQuestions);

// 원래 모드로 돌아가고 모달 제목 복원
setIsAddMode(false);
Expand All @@ -132,7 +135,7 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
options: {
enableFooter: false,
needsBackButton: true,
backButtonCallback: onClose,
backButtonCallback: handleCancel,
},
}));

Expand All @@ -148,11 +151,7 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
questionContent: content,
}));
const newQuestions = [...questions, ...newQuestionObjects];
if (isInitializedCreateSpace) {
setRetroCreateData((prev) => ({ ...prev, questions: newQuestions }));
} else {
setRetrospectQuestions(newQuestions);
}
setEditingQuestions(newQuestions);

// 원래 모드로 돌아가고 모달 제목 복원
setIsAddMode(false);
Expand All @@ -165,31 +164,49 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
toast.success(`${contents.length}개의 질문이 추가되었어요!`);
};

/**
* 질문 수정 취소 핸들러 (뒤로가기 버튼)
*/
const handleCancel = () => {
const hasChanged = !isEqual(originalQuestions, editingQuestions);

if (hasChanged) {
openExitWarningModal({
title: "질문 수정을 취소하시겠어요?",
contents: "수정중인 내용은 모두 사라져요",
onConfirm: () => {
// 원본 질문으로 복원 (atom은 변경하지 않음, 로컬 상태만 버림)
setEditingQuestions(originalQuestions);
onClose();
},
options: {
buttonText: ["취소", "나가기"],
},
});
} else {
onClose();
}
};

/**
* 질문 추가 취소 핸들러
*/
const handleAddQuestionCancel = () => {
openExitWarningModal({
title: "질문 수정을 취소하시겠어요?",
contents: "수정중인 내용은 모두 사라져요",
title: "질문 추가를 취소하시겠어요?",
contents: "추가중인 내용은 모두 사라져요",
onConfirm: () => {
// 백업된 질문들로 복원
if (isInitializedCreateSpace) {
setRetroCreateData((prev) => ({ ...prev, questions: questions }));
} else {
setRetrospectQuestions(questions);
}

setEditingQuestions(backupQuestions);
setIsAddMode(false);
setBackupQuestions([]);
onClose();
setModalDataState((prev) => ({
...prev,
title: "질문 리스트",
options: {
enableFooter: false,
needsBackButton: true,
backButtonCallback: onClose,
backButtonCallback: handleCancel,
},
}));
},
Expand All @@ -212,11 +229,7 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
* 삭제 모드 취소 핸들러 (질문 복원)
*/
const handleDeleteModeCancel = () => {
if (isInitializedCreateSpace) {
setRetroCreateData((prev) => ({ ...prev, questions: backupQuestions }));
} else {
setRetrospectQuestions(backupQuestions);
}
setEditingQuestions(backupQuestions);
setIsDeleteMode(false);
setBackupQuestions([]);
toast.success("삭제가 취소되었어요!");
Expand All @@ -232,19 +245,37 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp

// 제출 완료 핸들러
const handleComplete = () => {
const hasChanged = !isEqual(backupQuestions, questions);
const hasChanged = !isEqual(originalQuestions, editingQuestions);

if (hasChanged || retroCreateData.hasChangedOriginal) {
// 수정된 질문들을 atom에 반영
if (isInitializedCreateSpace) {
setRetroCreateData((prev) => ({
...prev,
hasChangedOriginal: true,
isNewForm: true,
questions: editingQuestions,
hasChangedOriginal: hasChanged || prev.hasChangedOriginal,
isNewForm: hasChanged || prev.isNewForm,
formName: hasChanged ? `커스텀 템플릿` : prev.formName,
}));
} else {
setRetrospectQuestions(editingQuestions);
}

onClose();
};

// 모달의 뒤로가기 버튼 콜백을 handleCancel로 설정
useEffect(() => {
if (!isAddMode) {
setModalDataState((prev) => ({
...prev,
options: {
...prev.options,
backButtonCallback: handleCancel,
},
}));
}
}, [editingQuestions, isAddMode]);

return (
<>
{isAddMode ? (
Expand Down Expand Up @@ -275,7 +306,13 @@ export default function QuestionEditSection({ onClose }: QuestionEditSectionProp
<Spacing size={1.2} />

{/* ---------- 메인 질문 리스트 ---------- */}
<MainQuestionsContents questions={questions} isDeleteMode={isDeleteMode} handleDelete={handleDelete} handleDragEnd={handleDragEnd} />
<MainQuestionsContents
questions={questions}
isDeleteMode={isDeleteMode}
handleDelete={handleDelete}
handleDragEnd={handleDragEnd}
handleContentChange={handleContentChange}
/>

{/* ---------- 추가 버튼 ---------- */}
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ export function ConfirmDefaultTemplate() {
}, [title, retroCreateData.hasChangedOriginal, retroCreateData.formName]);

useEffect(() => {
// * 이미 질문이 수정된 상태라면 원본으로 덮어쓰지 않음
if (retroCreateData.hasChangedOriginal) return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!! 이거를 발견한거구나?!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다!


setRetroCreateData((prev) => ({
...prev,
questions,
curFormId: Number(templateId),
}));
}, [questions, templateId, setRetroCreateData]);
}, [questions, templateId, setRetroCreateData, retroCreateData.hasChangedOriginal]);

const handleChangeTemplate = () => {
openActionModal({
Expand Down