Skip to content

feat: 기록 작성 / 투표 생성 기능 구현#67

Merged
ljh130334 merged 15 commits into
developfrom
feat/recordwrite
Jul 31, 2025
Merged

feat: 기록 작성 / 투표 생성 기능 구현#67
ljh130334 merged 15 commits into
developfrom
feat/recordwrite

Conversation

@ljh130334

@ljh130334 ljh130334 commented Jul 31, 2025

Copy link
Copy Markdown
Member

#️⃣연관된 이슈

#66

📝작업 내용

사용자가 읽은 내용에 대한 감상을 기록하고, 모임원들과 의견을 나누기 위한 투표를 생성할 수 있는 기능을 개발했습니다.

👩🏻‍💻 주요 구현 사항

1. 기록 작성 페이지 (RecordWrite)

  • 사용자가 읽은 페이지 범위를 설정할 수 있는 PageRangeSection 구현
  • 자동 높이 조절이 되는 텍스트 입력 영역 RecordContentSection 개발
  • 읽기 진행도가 80% 이상일 때만 '총평' 기능을 활성화하는 조건부 로직 구현
  • 페이지 입력이 없을 경우 마지막 기록 페이지를 기본값으로 사용하는 fallback 처리

2. 투표 생성 페이지 (PollWrite)

  • 투표 질문과 최대 5개까지의 선택지를 입력할 수 있는 PollCreationSection 구현
  • 동적으로 선택지를 추가/삭제할 수 있는 UI 제공
  • 기존 PageRangeSection 컴포넌트를 재사용하여 개발 효율성 향상

3. 기록장 연동

  • 작성된 기록이 Memory 페이지에 실시간으로 반영되는 state 관리 구현
  • location.state를 활용해 페이지 간 데이터 전달 및 중복 방지 로직
  • 새로 작성한 기록을 '내 기록' 탭으로 자동 이동하는 UX 개선

⚠️ 기술적 난관 및 해결 방법

1. 텍스트 영역 자동 높이 조절

초기에는 textarea의 높이가 고정되어 있어 긴 텍스트 입력 시 사용성이 떨어지는 문제가 있었습니다. 이를 해결하기 위해 useRef와 useEffect를 활용하여 내용 변경 시마다 scrollHeight를 기준으로 높이를 동적으로 조절하는 로직을 구현했습니다.

2. 상태 관리 및 페이지 간 데이터 전달

작성된 기록이 Memory 페이지에 즉시 반영되어야 하는 요구사항이 있었습니다. React Router의 navigate를 통한 state 전달과 useEffect를 활용한 상태 동기화, 그리고 중복 추가를 방지하기 위한 ID 기반 중복 검사 로직을 구현하여 해결했습니다.

3. 컴포넌트 재사용성 확보

PageRangeSection의 경우 기록 작성과 투표 생성 모두에서 사용되는 공통 컴포넌트였습니다. props 인터페이스를 명확히 정의하고, 각 페이지의 요구사항에 맞는 조건부 렌더링을 통해 재사용성을 확보했습니다.

4. 투표 옵션 동적 관리

사용자가 투표 선택지를 자유롭게 추가/삭제할 수 있도록 배열 상태 관리와 인덱스 기반 핸들러를 구현했습니다. 특히 삭제 시 참조 배열(inputRefs)도 함께 동기화하여 메모리 누수를 방지했습니다.

스크린샷 (선택)

2025-07-31.3.44.23.mov

Summary by CodeRabbit

  • New Features

    • 독서 기록 및 투표 작성 기능이 새롭게 추가되었습니다.
    • 기록 작성 페이지에서 페이지 범위 입력, 전체 범위 토글, 내용 입력 및 글자 수 표시 기능을 제공합니다.
    • 투표 작성 페이지에서는 최대 5개 옵션 입력, 옵션 추가/삭제, 전체 범위 적용 토글 기능이 지원됩니다.
    • 기록 목록이 개인 기록과 그룹 기록으로 분리되어 탭으로 전환할 수 있습니다.
    • 페이지 범위 설정 시 필터링이 가능하며, 기록 목록 내 페이지 정보 또는 총평 여부가 명확히 표시됩니다.
  • UI/UX 개선

    • 기록 목록 화면에 고정 헤더와 스크롤 구간 분리, 플로팅 버튼 등이 적용되어 사용성 및 레이아웃이 개선되었습니다.
    • 기록 추가 버튼이 내비게이션 기반으로 변경되어 자연스러운 이동이 가능해졌습니다.
    • 기록 및 투표 작성 화면에 일관된 스타일과 사용자 친화적인 입력 UI가 적용되었습니다.
  • 버그 수정 및 기타

    • 폼 입력값 검증 및 에러 메시지 표시가 강화되어 안정적인 입력 환경을 제공합니다.
    • 페이지 범위 및 전체 범위 토글 활성화 조건이 개선되어 사용 편의성이 향상되었습니다.

@vercel

vercel Bot commented Jul 31, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
thip ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 31, 2025 6:51am

@coderabbitai

coderabbitai Bot commented Jul 31, 2025

Copy link
Copy Markdown

Walkthrough

이 변경사항은 독서 기록 및 투표 생성 기능을 위한 신규 페이지와 컴포넌트 도입, 기존 메모리(기억) 기능의 구조적 리팩토링, UI/UX 개선, 상태 관리 방식 변경 등을 포함합니다. 주요 변화로는 기록/투표 작성 페이지와 관련 입력 섹션, 스타일 컴포넌트 추가, 그룹/내 기록 분리, 필터 및 페이지 범위 기능 강화 등이 있습니다.

Changes

Cohort / File(s) Change Summary
MemoryAddButton 리팩토링
src/components/memory/MemoryAddButton/MemoryAddButton.tsx
onAddRecord prop 제거, 외부 콜백 대신 react-router-domuseNavigate로 경로 이동 처리. 컴포넌트는 더 이상 props를 받지 않음.
MemoryContent 구조 개선 및 필터링 확장
src/components/memory/MemoryContent/MemoryContent.tsx, src/components/memory/MemoryContent/MemoryContent.styled.ts, src/components/memory/RecordFilters/RecordFilters.tsx
레이아웃을 고정/스크롤 영역으로 분리, 페이지 범위 필터링 기능 강화, onPageRangeSet prop 추가, 스타일 컴포넌트 확장 및 신규 추가.
RecordItem 페이지 정보 표시 개선
src/components/memory/RecordItem/RecordItem.tsx, src/components/memory/RecordItem/RecordItem.styled.ts
UserPointsPageInfo로 네이밍 변경, 페이지 정보 표시 방식 개선(총평/범위/0p 등).
PollWrite(투표 작성) 신규 도입
src/pages/pollwrite/PollWrite.tsx, src/pages/pollwrite/PollWrite.styled.ts, src/components/pollwrite/PollCreationSection.tsx, src/components/pollwrite/PollCreationSection.styled.ts
투표 생성 페이지 및 입력 섹션 컴포넌트 신규 추가, 옵션 추가/삭제/입력/포커스 관리, 스타일링 일관화.
RecordWrite(기록 작성) 신규 도입
src/pages/recordwrite/RecordWrite.tsx, src/pages/recordwrite/RecordWrite.styled.ts, src/components/recordwrite/PageRangeSection.tsx, src/components/recordwrite/PageRangeSection.styled.ts, src/components/recordwrite/RecordContentSection.tsx, src/components/recordwrite/RecordContentSection.styled.ts, src/components/recordwrite/VoiceToggleSection.tsx, src/components/recordwrite/VoiceToggleSection.styled.ts
기록 작성 페이지 및 입력 섹션, 범위 입력, 총평 토글, 음성 토글 등 신규 컴포넌트 도입 및 스타일링.
메모리 페이지 구조 및 상태 관리 리팩토링
src/pages/memory/Memory.tsx, src/pages/memory/Memory.styled.ts
내 기록/그룹 기록 분리, 라우터 상태 기반 기록 추가, 필터링 로직 강화, UI 고정/플로팅/스크롤 영역 분리, 스타일 컴포넌트 확장.
라우팅 확장
src/pages/index.tsx
/memory/record/write, /memory/poll/write 경로 및 해당 컴포넌트 라우팅 추가.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MemoryPage
  participant Router
  participant RecordWrite
  participant PollWrite

  User->>MemoryPage: "기록 추가" 버튼 클릭
  MemoryPage->>Router: /memory/record/write로 이동
  Router->>RecordWrite: 기록 작성 페이지 렌더
  User->>RecordWrite: 기록 입력 및 완료 클릭
  RecordWrite->>MemoryPage: 새 기록 전달하며 /memory로 이동

  User->>MemoryPage: "투표 추가" 버튼 클릭
  MemoryPage->>Router: /memory/poll/write로 이동
  Router->>PollWrite: 투표 작성 페이지 렌더
  User->>PollWrite: 투표 내용/옵션 입력 및 완료 클릭
  PollWrite->>MemoryPage: 새 투표 전달하며 /memory로 이동
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

Possibly related PRs

  • feat: 기록장 페이지 구현 #65: 기존 MemoryAddButton 컴포넌트에 onAddRecord 콜백 prop을 도입하고 드롭다운 UI를 구현한 PR로, 이번 PR에서 해당 컴포넌트의 상호작용 모델이 리팩토링됨.

Suggested labels

✨ Feature, 🎨 Html&css

Poem

🐇
새로워진 기록과 투표의 길,
페이지 범위, 총평 토글,
스타일도 반짝, 입력도 척척!
고정된 헤더, 떠있는 버튼,
토끼는 뛴다, 새로운 기억을 품고
리뷰어님, 이 변화의 숲을
한 번만 더 뛰어넘어 주세요!
📝✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8f8282 and ef5bf10.

📒 Files selected for processing (1)
  • src/pages/memory/Memory.tsx (10 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/pages/memory/Memory.tsx (1)
src/pages/memory/Memory.styled.ts (3)
  • FixedHeader (16-21)
  • ScrollableContent (23-46)
  • FloatingElements (48-58)
🔇 Additional comments (4)
src/pages/memory/Memory.tsx (4)

1-2: React import 명시적 추가와 useLocation 도입 확인됨

React의 명시적 import와 새로운 기능을 위한 useLocation 훅 추가가 적절합니다.


239-271: 레이아웃 구조 개선이 잘 구현되었습니다

FixedHeader, ScrollableContent, FloatingElements로 구성된 새로운 레이아웃 구조가 UI/UX를 크게 개선했습니다. 스크롤 영역과 고정 요소들이 적절히 분리되어 있고, 플로팅 요소들의 포인터 이벤트 관리도 잘 되어 있습니다.


256-256: 새로운 onPageRangeSet 콜백 추가 확인

handlePageRangeSet 함수를 onPageRangeSet prop으로 전달하는 것이 적절하게 구현되었습니다. 페이지 범위 필터링 기능을 위한 상태 업데이트가 올바르게 연결되어 있습니다.


111-136: 새 기록 추가 로직의 잠재적 메모리 누수 위험

useEffect의 의존성 배열에 location.state?.newRecord?.id를 사용하고 있지만, newRecord 객체 전체가 변경될 때마다 effect가 실행될 수 있습니다. 또한 navigate 호출 후 즉시 state를 초기화하는 방식이 race condition을 유발할 수 있습니다.

- }, [location.state?.newRecord?.id, navigate, location.pathname]);
+ }, [location.state?.newRecord, navigate, location.pathname]);

그리고 state 초기화를 effect 내에서 setTimeout으로 지연시키거나, cleanup 함수에서 처리하는 것을 권장합니다.

Likely an incorrect or invalid review comment.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/recordwrite

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🔭 Outside diff range comments (1)
src/pages/memory/Memory.tsx (1)

54-108: 프로덕션 코드에 더미 데이터가 포함되어 있습니다.

개발용 더미 데이터가 프로덕션 코드에 남아있습니다. 실제 배포 시에는 빈 배열로 초기화하거나 API에서 데이터를 가져오도록 수정해야 합니다.

-  const [groupRecords, setGroupRecords] = useState<Record[]>([
-    {
-      id: '1',
-      user: 'user.01',
-      // ... 더미 데이터
-    },
-    // ... 더 많은 더미 데이터
-  ]);
+  const [groupRecords, setGroupRecords] = useState<Record[]>([]);
🧹 Nitpick comments (7)
src/components/memory/MemoryAddButton/MemoryAddButton.tsx (1)

37-37: 프로덕션 코드에서 console.log 제거가 필요합니다.

개발용 console.log 문이 남아있습니다. 프로덕션 배포 전에 제거해야 합니다.

  const handleRecordWrite = () => {
    setIsOpen(false);
    navigate('/memory/record/write');
-   console.log('기록 작성하기');
  };

  const handlePollCreate = () => {
    setIsOpen(false);
    navigate('/memory/poll/write');
-   console.log('투표 생성하기');
  };

Also applies to: 43-43

src/components/recordwrite/RecordContentSection.tsx (1)

30-32: useEffect 최적화 고려사항

두 개의 useEffect를 하나로 통합할 수 있습니다. 현재 구조는 작동하지만, 약간 중복적입니다.

다음과 같이 최적화할 수 있습니다:

-  // content가 변경될 때마다 높이 조절
-  useEffect(() => {
-    adjustHeight();
-  }, [content]);
-
-  // 컴포넌트 마운트 시 초기 높이 설정
-  useEffect(() => {
-    adjustHeight();
-  }, []);
+  // content 변경 시와 마운트 시 높이 조절
+  useEffect(() => {
+    adjustHeight();
+  }, [content]);

Also applies to: 35-37

src/pages/memory/Memory.styled.ts (2)

11-14: 모바일 환경에서의 뷰포트 높이 문제를 고려해보세요.

height: 100vh는 일부 모바일 브라우저에서 주소창 높이 변화로 인해 레이아웃 문제를 일으킬 수 있습니다.

더 안정적인 대안을 고려해보세요:

- height: 100vh;
+ min-height: 100vh;
+ min-height: 100dvh; /* 지원되는 브라우저에서 동적 뷰포트 높이 사용 */

23-46: 매직 넘버를 상수로 분리하고 스크롤바 호환성을 개선하세요.

스크롤 가능한 콘텐츠 영역의 구현은 좋지만 몇 가지 개선점이 있습니다.

개선 사항:

+ // AddButton 높이를 상수로 정의
+ const ADD_BUTTON_HEIGHT = 100;
+
 export const ScrollableContent = styled.div`
   flex: 1;
   overflow-y: auto;
   overflow-x: hidden;
-  padding-bottom: 100px; /* AddButton 공간 확보 */
+  padding-bottom: ${ADD_BUTTON_HEIGHT}px; /* AddButton 공간 확보 */

   /* 스크롤바 스타일링 */
+  scrollbar-width: thin; /* Firefox 지원 */
+  scrollbar-color: ${semanticColors.text.ghost} transparent; /* Firefox 지원 */
+  
   &::-webkit-scrollbar {
     width: 4px;
   }
src/pages/pollwrite/PollWrite.tsx (1)

10-24: 상태 관리를 useReducer로 개선하고 상수를 분리하세요.

여러 개의 관련된 상태를 개별 useState로 관리하고 있고, 플레이스홀더 값들이 하드코딩되어 있습니다.

상태 관리 개선:

+ // 상수 분리
+ const PLACEHOLDER_VALUES = {
+   TOTAL_PAGES: 600,
+   LAST_RECORDED_PAGE: 456, 
+   READING_PROGRESS: 70,
+ } as const;
+
+ // 상태 타입 정의
+ interface PollWriteState {
+   pageRange: string;
+   pollContent: string;
+   pollOptions: string[];
+   isOverallEnabled: boolean;
+   isSubmitting: boolean;
+ }
+
+ // useReducer로 통합 관리 고려
- const [pageRange, setPageRange] = useState('');
- const [pollContent, setPollContent] = useState('');
- const [pollOptions, setPollOptions] = useState(['', '']);
- const [isOverallEnabled, setIsOverallEnabled] = useState(false);
- const [isSubmitting, setIsSubmitting] = useState(false);
src/components/pollwrite/PollCreationSection.tsx (1)

28-31: 상수를 파일 최상단으로 이동하세요.

매직 넘버들이 컴포넌트 내부에 정의되어 있습니다.

상수 정의 개선:

+ // 상수를 파일 최상단으로 이동
+ const MAX_CONTENT_LENGTH = 20;
+ const MAX_OPTIONS = 5;
+
 const PollCreationSection = ({
   // ...
 }: PollCreationSectionProps) => {
-  const maxContentLength = 20;
-  const maxOptions = 5;
src/components/recordwrite/PageRangeSection.tsx (1)

101-101: 오류 메시지를 국제화 고려하여 개선하세요.

하드코딩된 한국어 오류 메시지가 있습니다.

국제화를 고려한 메시지 처리:

+ // 상수로 메시지 정의하거나 i18n 라이브러리 사용
+ const ERROR_MESSAGES = {
+   PAGE_EXCEED: '전체페이지를 초과할 수 없어요.',
+ } as const;
+
- {!isOverallEnabled && hasError && <ErrorMessage>전체페이지를 초과할 수 없어요.</ErrorMessage>}
+ {!isOverallEnabled && hasError && <ErrorMessage>{ERROR_MESSAGES.PAGE_EXCEED}</ErrorMessage>}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3f367 and c8f8282.

⛔ Files ignored due to path filters (3)
  • src/assets/common/delete.svg is excluded by !**/*.svg
  • src/assets/common/infoIcon.svg is excluded by !**/*.svg
  • src/assets/common/trash.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • src/components/memory/MemoryAddButton/MemoryAddButton.tsx (2 hunks)
  • src/components/memory/MemoryContent/MemoryContent.styled.ts (1 hunks)
  • src/components/memory/MemoryContent/MemoryContent.tsx (4 hunks)
  • src/components/memory/RecordFilters/RecordFilters.tsx (3 hunks)
  • src/components/memory/RecordItem/RecordItem.styled.ts (1 hunks)
  • src/components/memory/RecordItem/RecordItem.tsx (3 hunks)
  • src/components/pollwrite/PollCreationSection.styled.ts (1 hunks)
  • src/components/pollwrite/PollCreationSection.tsx (1 hunks)
  • src/components/recordwrite/PageRangeSection.styled.ts (1 hunks)
  • src/components/recordwrite/PageRangeSection.tsx (1 hunks)
  • src/components/recordwrite/RecordContentSection.styled.ts (1 hunks)
  • src/components/recordwrite/RecordContentSection.tsx (1 hunks)
  • src/components/recordwrite/VoiceToggleSection.styled.ts (1 hunks)
  • src/components/recordwrite/VoiceToggleSection.tsx (1 hunks)
  • src/pages/index.tsx (2 hunks)
  • src/pages/memory/Memory.styled.ts (1 hunks)
  • src/pages/memory/Memory.tsx (10 hunks)
  • src/pages/pollwrite/PollWrite.styled.ts (1 hunks)
  • src/pages/pollwrite/PollWrite.tsx (1 hunks)
  • src/pages/recordwrite/RecordWrite.styled.ts (1 hunks)
  • src/pages/recordwrite/RecordWrite.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (13)
src/components/memory/MemoryContent/MemoryContent.tsx (1)
src/components/memory/MemoryContent/MemoryContent.styled.ts (3)
  • Content (4-8)
  • FixedSection (10-20)
  • ScrollableSection (22-25)
src/components/recordwrite/VoiceToggleSection.tsx (1)
src/components/recordwrite/VoiceToggleSection.styled.ts (7)
  • Container (4-7)
  • ToggleContainer (9-13)
  • LeftSection (15-19)
  • InfoIcon (21-33)
  • ToggleLabel (35-39)
  • ToggleSwitch (41-50)
  • ToggleSlider (52-61)
src/pages/pollwrite/PollWrite.styled.ts (1)
src/styles/global/global.ts (1)
  • semanticColors (80-153)
src/components/memory/RecordItem/RecordItem.tsx (1)
src/components/memory/RecordItem/RecordItem.styled.ts (1)
  • PageInfo (39-43)
src/components/recordwrite/RecordContentSection.tsx (1)
src/components/recordwrite/RecordContentSection.styled.ts (5)
  • Section (4-8)
  • SectionTitle (10-15)
  • TextAreaBox (17-21)
  • TextArea (23-60)
  • CharacterCount (62-68)
src/pages/recordwrite/RecordWrite.tsx (2)
src/pages/memory/Memory.tsx (1)
  • Record (13-26)
src/pages/recordwrite/RecordWrite.styled.ts (1)
  • Container (4-15)
src/components/pollwrite/PollCreationSection.styled.ts (1)
src/styles/global/global.ts (3)
  • semanticColors (80-153)
  • typography (56-77)
  • colors (4-53)
src/pages/pollwrite/PollWrite.tsx (2)
src/pages/memory/Memory.tsx (1)
  • Record (13-26)
src/pages/pollwrite/PollWrite.styled.ts (1)
  • Container (4-15)
src/pages/memory/Memory.styled.ts (1)
src/styles/global/global.ts (1)
  • semanticColors (80-153)
src/components/pollwrite/PollCreationSection.tsx (1)
src/components/pollwrite/PollCreationSection.styled.ts (8)
  • Section (4-8)
  • PollContentContainer (10-12)
  • PollInput (14-28)
  • PollOptionsContainer (30-34)
  • OptionInputContainer (36-43)
  • OptionInput (45-59)
  • DeleteButton (61-81)
  • AddOptionButton (83-103)
src/components/recordwrite/PageRangeSection.styled.ts (3)
src/components/recordwrite/RecordContentSection.styled.ts (2)
  • Section (4-8)
  • SectionTitle (10-15)
src/styles/global/global.ts (2)
  • semanticColors (80-153)
  • typography (56-77)
src/components/recordwrite/VoiceToggleSection.styled.ts (6)
  • ToggleContainer (9-13)
  • LeftSection (15-19)
  • InfoIcon (21-33)
  • ToggleLabel (35-39)
  • ToggleSwitch (41-50)
  • ToggleSlider (52-61)
src/pages/memory/Memory.tsx (1)
src/pages/memory/Memory.styled.ts (3)
  • FixedHeader (16-21)
  • ScrollableContent (23-46)
  • FloatingElements (48-58)
src/components/recordwrite/VoiceToggleSection.styled.ts (2)
src/components/recordwrite/PageRangeSection.styled.ts (6)
  • ToggleContainer (107-113)
  • LeftSection (115-119)
  • InfoIcon (121-132)
  • ToggleLabel (134-139)
  • ToggleSwitch (141-153)
  • ToggleSlider (155-166)
src/styles/global/global.ts (2)
  • semanticColors (80-153)
  • typography (56-77)
🔇 Additional comments (35)
src/components/recordwrite/RecordContentSection.styled.ts (1)

1-69: 잘 구현된 스타일 컴포넌트입니다

테마 토큰을 일관되게 사용하고, 텍스트영역의 UX를 고려한 스타일링(크기 조절 비활성화, 부드러운 전환, 커스텀 스크롤바)이 잘 적용되어 있습니다. 문자 수 표시와 플레이스홀더 스타일링도 적절합니다.

src/components/recordwrite/PageRangeSection.styled.ts (1)

1-167: 포괄적이고 잘 구현된 스타일 컴포넌트입니다

페이지 범위 입력을 위한 스타일 컴포넌트가 체계적으로 구현되어 있습니다. 에러 상태 처리, 비활성화 상태 스타일링, 조건부 스타일링이 적절히 적용되어 있고, TypeScript 타이핑도 올바르게 되어 있습니다.

참고: VoiceToggleSection.styled.ts와의 코드 중복 문제는 해당 파일에서 이미 언급했습니다.

src/components/memory/RecordItem/RecordItem.styled.ts (1)

39-39: 의미에 맞는 컴포넌트 이름 변경입니다

UserPoints에서 PageInfo로의 이름 변경이 새로운 기능(페이지 정보 표시)에 더 적합합니다. 스타일 속성은 그대로 유지되어 일관성이 보장됩니다.

src/pages/index.tsx (1)

29-30: 라우팅 구조가 논리적으로 잘 구성되었습니다

새로운 RecordWritePollWrite 페이지가 /memory 경로 하위에 적절히 배치되어 있습니다. 라우트 구조가 기존 패턴과 일관성을 유지하며, import 문도 올바르게 추가되었습니다.

Also applies to: 55-56

src/pages/pollwrite/PollWrite.styled.ts (1)

4-15: 잘 구현된 컨테이너 스타일 컴포넌트입니다.

반응형 디자인과 일관된 스타일링이 잘 적용되었습니다. 시맨틱 컬러 시스템을 올바르게 사용하고 있으며, 다른 페이지의 컨테이너 컴포넌트와 일관성을 유지하고 있습니다.

src/components/memory/MemoryAddButton/MemoryAddButton.tsx (1)

8-9: 네비게이션 통합이 잘 구현되었습니다.

콜백 프롭 대신 useNavigate 훅을 사용하여 라우팅과 직접 통합한 것은 좋은 개선입니다. 외부 의존성을 제거하고 컴포넌트를 더 독립적으로 만들었습니다.

src/components/memory/RecordItem/RecordItem.tsx (2)

27-37: 프롭 구조 분해가 잘 업데이트되었습니다.

새로운 pageRangerecordType 속성을 올바르게 추가하고 기존의 userPoints를 제거한 것이 적절합니다. 페이지 범위 필터링과 전체 기록 구분 기능과 잘 통합됩니다.


55-63: 페이지 정보 표시 로직이 잘 구현되었습니다.

renderPageInfo 헬퍼 함수가 명확한 조건부 로직을 제공합니다:

  • 총평 기록에 대한 특별한 처리
  • 페이지 범위가 있을 때의 표시
  • 적절한 기본값 제공

조건부 렌더링이 깔끔하고 다양한 기록 유형을 잘 처리합니다.

src/components/recordwrite/VoiceToggleSection.tsx (2)

12-15: 명확한 인터페이스 정의입니다.

VoiceToggleSectionProps 인터페이스가 간단하고 명확합니다. boolean 상태와 콜백 함수로 구성된 깔끔한 API를 제공합니다.


17-33: 재사용 가능한 토글 컴포넌트가 잘 구현되었습니다.

컴포넌트 구조가 깔끔하고 다음과 같은 장점이 있습니다:

  • 관심사의 분리가 잘 되어 있음
  • 스타일 컴포넌트의 적절한 사용
  • 시각적 피드백을 위한 active 상태 전달
  • 접근성을 고려한 alt 텍스트 제공

부모 컴포넌트와의 통합이 잘 될 것으로 보입니다.

src/components/memory/RecordFilters/RecordFilters.tsx (1)

16-16: 페이지 범위 설정 콜백 추가가 적절합니다.

새로운 onPageRangeSet 콜백 prop 추가가 깔끔하게 구현되었습니다. 선택사항으로 설정하여 하위 호환성을 유지하면서, 상위 컴포넌트에 페이지 범위 변경을 알릴 수 있는 기능을 제공합니다.

Also applies to: 27-27, 69-72

src/components/recordwrite/RecordContentSection.tsx (2)

20-27: 텍스트 영역 높이 자동 조절 로직이 정확합니다.

adjustHeight 함수가 올바르게 구현되었습니다. height를 'auto'로 초기화한 후 scrollHeight로 설정하는 방식은 동적 높이 조절의 표준 패턴입니다.


55-57: 문자 수 표시 기능이 잘 구현되었습니다.

현재 문자 수와 최대 문자 수를 명확하게 표시하여 사용자 경험을 향상시킵니다.

src/pages/recordwrite/RecordWrite.tsx (2)

28-32: 중복 실행 방지 로직이 적절합니다.

isSubmitting 상태를 사용한 중복 실행 방지와 early return 패턴이 잘 구현되었습니다.


35-39: 페이지 범위 결정 로직이 합리적입니다.

총평 모드일 때는 페이지 범위를 undefined로 설정하고, 입력값이 없을 때 마지막 기록 페이지를 기본값으로 사용하는 로직이 사용자 친화적입니다.

src/components/pollwrite/PollCreationSection.styled.ts (2)

1-103: 스타일 컴포넌트가 체계적으로 잘 구성되었습니다.

전체적으로 다음과 같은 장점들이 있습니다:

  • semantic colors와 typography 변수를 일관되게 사용하여 디자인 시스템과의 일관성 유지
  • 적절한 hover 상태와 transition 효과 구현
  • 접근성을 고려한 버튼 스타일링
  • 반응형을 고려한 flex 레이아웃 사용

97-102: 버튼 호버 효과가 사용자 경험을 향상시킵니다.

AddOptionButton의 호버 시 색상 변화와 부드러운 transition이 직관적인 상호작용을 제공합니다.

src/components/memory/MemoryContent/MemoryContent.tsx (3)

22-22: 페이지 범위 설정 prop 전달이 올바르게 구현되었습니다.

새로운 onPageRangeSet prop이 적절히 추가되고 RecordFilters 컴포넌트에 전달되어 상위-하위 컴포넌트 간 데이터 흐름이 잘 구성되었습니다.

Also applies to: 38-38, 60-60


47-63: 고정/스크롤 영역 분리가 UX를 개선합니다.

레이아웃을 FixedSectionScrollableSection으로 분리한 리팩토링이 훌륭합니다. 탭과 필터는 고정되어 접근성을 높이고, 콘텐츠 영역은 스크롤 가능하여 사용자 경험이 향상됩니다.

Also applies to: 66-75


51-52: 조건부 렌더링 로직이 명확합니다.

그룹 탭일 때만 필터를 표시하고, 그룹 탭이면서 기록이 있을 때만 안내 메시지를 표시하는 조건부 렌더링이 논리적이고 명확합니다.

Also applies to: 68-68

src/pages/memory/Memory.styled.ts (2)

16-21: LGTM!

고정 헤더 구현이 적절합니다. sticky 포지셔닝과 적절한 z-index 설정으로 스크롤 시에도 헤더가 상단에 고정됩니다.


48-58: LGTM!

플로팅 엘리먼트의 포인터 이벤트 관리가 잘 구현되었습니다. 컨테이너는 클릭 이벤트를 차단하고 직접 자식 요소만 상호작용할 수 있도록 한 설계가 우수합니다.

src/components/memory/MemoryContent/MemoryContent.styled.ts (1)

4-8: LGTM!

Content 컴포넌트의 단순화가 적절합니다. 레이아웃 책임을 하위 컴포넌트로 분리한 것이 좋은 설계입니다.

src/pages/pollwrite/PollWrite.tsx (2)

87-89: LGTM!

폼 유효성 검사 로직이 적절합니다. 투표 내용이 있고 최소 2개의 옵션이 입력되었는지 확인하는 조건이 정확합니다.


91-120: LGTM!

컴포넌트 렌더링 구조가 잘 설계되었습니다. TitleHeader와 두 개의 섹션 컴포넌트로 깔끔하게 분리되어 있고, props 전달도 적절합니다.

src/components/pollwrite/PollCreationSection.tsx (2)

101-127: LGTM!

동적 옵션 렌더링 로직이 잘 구현되었습니다. 조건부 버튼 표시와 인덱스 기반 삭제 로직이 사용자 경험을 잘 고려했습니다.


64-74: refs 배열 관리를 개선하세요.

수동으로 refs 배열을 관리하는 것은 메모리 누수를 발생시킬 수 있습니다.

refs 관리 개선:

+ // useCallback으로 ref 함수 최적화
+ const setInputRef = useCallback((index: number) => (el: HTMLInputElement | null) => {
+   inputRefs.current[index] = el;
+ }, []);

 const handleRemoveOption = (index: number) => {
   if (pollOptions.length > 2) {
     const newOptions = pollOptions.filter((_, i) => i !== index);
     const newFocusStates = focusStates.filter((_, i) => i !== index);
     onPollOptionsChange(newOptions);
     setFocusStates(newFocusStates);

-    // refs 배열도 업데이트
-    inputRefs.current = inputRefs.current.filter((_, i) => i !== index);
+    // refs 배열 크기 조정
+    inputRefs.current.length = newOptions.length;
   }
 };

Likely an incorrect or invalid review comment.

src/components/recordwrite/PageRangeSection.tsx (2)

44-45: LGTM!

읽기 진행도 기반의 총평 활성화 로직이 적절합니다. 80% 임계값 설정이 합리적입니다.


69-74: LGTM!

토글 클릭 핸들러가 조건부로 실행되도록 잘 구현되었습니다. 진행도가 부족할 때 아무 동작하지 않는 것이 적절합니다.

src/pages/memory/Memory.tsx (6)

1-2: Import 구조 개선이 적절합니다!

React를 명시적으로 import하고 필요한 hooks와 styled components를 추가한 것이 좋습니다.

Also applies to: 8-8


139-145: 효율적인 기록 분리 로직입니다!

탭에 따라 적절한 기록을 반환하는 로직이 깔끔하게 구현되었습니다.


229-231: 페이지 범위 설정 핸들러가 적절히 구현되었습니다.


233-235: 기록 추가 네비게이션이 올바르게 구현되었습니다.

콘솔 로그 대신 실제 기록 작성 페이지로 이동하도록 변경된 것이 좋습니다.


242-275: UI 구조 개선이 훌륭합니다!

고정 헤더, 스크롤 가능한 콘텐츠, 플로팅 요소들을 명확하게 분리하여 사용자 경험이 향상되었습니다. hasRecords prop 계산도 각 탭의 특성에 맞게 적절히 구현되었습니다.


110-136: useEffect 의존성 배열 최적화 필요

의존성 배열에 location.state?.newRecord?.id를 사용하면 newRecord가 없을 때 effect가 다시 실행되지 않을 수 있습니다. location.state를 직접 의존성으로 사용하는 것이 더 안전합니다.

-  }, [location.state?.newRecord?.id, navigate, location.pathname]);
+  }, [location.state, navigate, location.pathname]);

Likely an incorrect or invalid review comment.

Comment on lines +10 to +20
export const FixedSection = styled.div`
position: sticky;
top: 0;
background-color: ${semanticColors.background.primary};
z-index: 5;
padding: 20px 0 16px 0;
margin: 0 -20px;
padding-left: 20px;
padding-right: 20px;
padding-top: 56px;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

복잡한 패딩 계산을 단순화하세요.

FixedSection의 패딩 설정이 복잡하고 매직 넘버를 포함하고 있습니다.

패딩 로직 개선:

+ // 헤더 관련 상수들을 정의
+ const HEADER_HEIGHT = 56;
+ const SECTION_PADDING = 20;
+ 
 export const FixedSection = styled.div`
   position: sticky;
   top: 0;
   background-color: ${semanticColors.background.primary};
   z-index: 5;
-  padding: 20px 0 16px 0;
-  margin: 0 -20px;
-  padding-left: 20px;
-  padding-right: 20px;
-  padding-top: 56px;
+  padding: ${HEADER_HEIGHT}px ${SECTION_PADDING}px 16px ${SECTION_PADDING}px;
+  margin: 0 -${SECTION_PADDING}px;
 `;
🤖 Prompt for AI Agents
In src/components/memory/MemoryContent/MemoryContent.styled.ts between lines 10
and 20, the FixedSection styled component has redundant and conflicting padding
declarations with magic numbers. Simplify the padding by consolidating all
padding properties into a single padding shorthand that clearly defines top,
right, bottom, and left values without duplication or conflicting values.

Comment on lines 27 to 44
export const DevButton = styled.button`
background-color: ${colors.red};
position: fixed;
top: 100px;
right: 20px;
background: ${semanticColors.button.fill.primary};
color: ${semanticColors.text.primary};
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: ${typography.fontSize.sm};
font-weight: ${typography.fontWeight.medium};
padding: 8px 12px;
border-radius: 4px;
font-size: ${typography.fontSize.xs};
cursor: pointer;
margin: 16px 0;
align-self: flex-start;
z-index: 1000;
z-index: 100;
opacity: 0.8;

&:hover {
opacity: 0.8;
opacity: 1;
}
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DevButton의 고정 위치가 다른 UI 요소와 충돌할 수 있습니다.

개발 버튼이 고정 위치에 배치되어 있어 다른 플로팅 요소들과 겹칠 가능성이 있습니다.

다른 고정/플로팅 요소들과의 충돌 여부를 확인해보세요:


🏁 Script executed:

#!/bin/bash
# Description: 다른 고정 위치 요소들을 검색하여 z-index 충돌 가능성 확인

# 고정 위치 요소들 검색
rg -A 3 "position:\s*(fixed|absolute)" --type ts --type tsx

# z-index 값들 검색  
rg -A 1 -B 1 "z-index:\s*\d+" --type ts --type tsx

Length of output: 170


아래 스크립트로 TS/TSX 파일 전반에 걸쳐 고정 위치 요소(fixed/absolute)와 z-index 값을 다시 검색해 충돌 가능성을 확인해 보세요.


🏁 Script executed:

#!/bin/bash
# Description: TS/TSX 파일에서 position(fixed|absolute) 및 z-index 검색

# 고정/절대 위치 요소 검색
rg -A 3 "position:\s*(fixed|absolute)" -g '*.ts' -g '*.tsx'

# z-index 값 검색
rg -A 1 -B 1 "z-index:\s*[0-9]+" -g '*.ts' -g '*.tsx'

Length of output: 24531


DevButton 고정 위치 및 z-index 충돌 위험
현재 src/components/memory/MemoryContent/MemoryContent.styled.ts에서 DevButton은

position: fixed;
top: 100px;
right: 20px;
z-index: 100;

으로 설정되어 있으나, 아래와 같은 요소들이 동일하거나 더 높은 z-index를 사용 중이라 DevButton이 가려지거나 겹칠 수 있습니다.

  • src/components/common/TitleHeader.tsx → z-index: 100
  • src/components/common/Modal/Snackbar.tsx → z-index: 1,100
  • src/components/common/Modal/PopupContainer.tsx → z-index: 1,000
  • src/pages/mypage/WithdrawPage.tsx → z-index: 2,000

제안

  • DevButton의 z-index 값을 더 높게 조정하거나
  • 위치(top/right) 조정
  • React Portal 등으로 별도 레이어에 렌더링

위 방안을 참고해 DevButton이 의도한 위치 위에 항상 노출되도록 수정해주세요.

🤖 Prompt for AI Agents
In src/components/memory/MemoryContent/MemoryContent.styled.ts between lines 27
and 44, the DevButton's fixed position with z-index 100 conflicts with other
elements having equal or higher z-index, causing it to be obscured. To fix this,
increase the z-index value of DevButton to a number higher than 2000 to ensure
it appears on top, or alternatively adjust its position or render it via a React
Portal to a separate layer so it is always visible above other components.

Comment on lines +84 to +87
if (focusStates.length !== pollOptions.length) {
const newFocusStates = pollOptions.map((_, index) => focusStates[index] || false);
setFocusStates(newFocusStates);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

렌더링 중 상태 업데이트를 useEffect로 이동하세요.

렌더링 중에 직접 상태를 업데이트하는 것은 성능 문제와 무한 렌더링을 야기할 수 있습니다.

useEffect로 동기화 로직 이동:

+ // useEffect로 focusStates 동기화
+ useEffect(() => {
+   if (focusStates.length !== pollOptions.length) {
+     const newFocusStates = pollOptions.map((_, index) => focusStates[index] || false);
+     setFocusStates(newFocusStates);
+   }
+ }, [pollOptions.length, focusStates.length]);

- // pollOptions 길이가 변경될 때 focusStates 동기화
- if (focusStates.length !== pollOptions.length) {
-   const newFocusStates = pollOptions.map((_, index) => focusStates[index] || false);
-   setFocusStates(newFocusStates);
- }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (focusStates.length !== pollOptions.length) {
const newFocusStates = pollOptions.map((_, index) => focusStates[index] || false);
setFocusStates(newFocusStates);
}
// useEffect로 focusStates 동기화
useEffect(() => {
if (focusStates.length !== pollOptions.length) {
const newFocusStates = pollOptions.map((_, index) => focusStates[index] || false);
setFocusStates(newFocusStates);
}
}, [pollOptions.length, focusStates.length]);
🤖 Prompt for AI Agents
In src/components/pollwrite/PollCreationSection.tsx around lines 84 to 87, the
state update for focusStates is done directly during rendering, which can cause
performance issues and infinite re-renders. Move this synchronization logic into
a useEffect hook that watches pollOptions and focusStates, and updates
focusStates only when their lengths differ. This ensures state updates happen
after render and prevents infinite loops.

Comment on lines +47 to +62
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;

// 숫자만 입력 허용
if (value === '' || /^\d+$/.test(value)) {
onPageRangeChange(value);

// 전체 페이지 수를 초과하면 에러 상태로 변경
if (value !== '') {
const page = parseInt(value);
setHasError(page > totalPages);
} else {
setHasError(false);
}
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 검증을 개선하세요.

숫자 입력 검증과 오류 처리에 몇 가지 개선점이 있습니다.

입력 검증 개선:

 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
   const value = e.target.value;

   // 숫자만 입력 허용
   if (value === '' || /^\d+$/.test(value)) {
     onPageRangeChange(value);

     // 전체 페이지 수를 초과하면 에러 상태로 변경
     if (value !== '') {
-      const page = parseInt(value);
+      const page = parseInt(value, 10);
-      setHasError(page > totalPages);
+      setHasError(page > totalPages || page < 1);
     } else {
       setHasError(false);
     }
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// 숫자만 입력 허용
if (value === '' || /^\d+$/.test(value)) {
onPageRangeChange(value);
// 전체 페이지 수를 초과하면 에러 상태로 변경
if (value !== '') {
const page = parseInt(value);
setHasError(page > totalPages);
} else {
setHasError(false);
}
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// 숫자만 입력 허용
if (value === '' || /^\d+$/.test(value)) {
onPageRangeChange(value);
// 전체 페이지 수를 초과하거나 1 페이지 미만이면 에러 상태로 변경
if (value !== '') {
const page = parseInt(value, 10);
setHasError(page > totalPages || page < 1);
} else {
setHasError(false);
}
}
};
🤖 Prompt for AI Agents
In src/components/recordwrite/PageRangeSection.tsx around lines 47 to 62,
improve the input validation by ensuring that the input only accepts valid
numeric values within the allowed range and properly handles edge cases. Update
the regex or validation logic to prevent invalid characters, and enhance the
error state management to correctly reflect when the input exceeds totalPages or
is otherwise invalid. Make sure to handle empty input gracefully without
triggering errors.

Comment on lines +9 to +61
export const ToggleContainer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;

export const LeftSection = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;

export const InfoIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;

img {
width: 16px;
height: 16px;
opacity: 0.6;
}
`;

export const ToggleLabel = styled.span`
color: ${semanticColors.text.primary};
font-size: ${typography.fontSize.base};
font-weight: ${typography.fontWeight.regular};
`;

export const ToggleSwitch = styled.div<{ active: boolean }>`
width: 48px;
height: 28px;
background-color: ${({ active }) =>
active ? semanticColors.button.fill.primary : semanticColors.background.card};
border-radius: 14px;
position: relative;
cursor: pointer;
transition: background-color 0.3s;
`;

export const ToggleSlider = styled.div<{ active: boolean }>`
width: 20px;
height: 20px;
background-color: ${semanticColors.text.primary};
border-radius: 50%;
position: absolute;
top: 4px;
left: ${({ active }) => (active ? '24px' : '4px')};
transition: left 0.3s;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

코드 중복을 해결하여 유지보수성을 개선하세요

PageRangeSection.styled.ts와 거의 동일한 토글 관련 컴포넌트들(ToggleContainer, LeftSection, InfoIcon, ToggleSwitch, ToggleSlider)이 중복 정의되어 있습니다.

공통 토글 컴포넌트를 별도 파일로 분리하여 재사용하는 것을 권장합니다:

+// src/components/common/Toggle/Toggle.styled.ts 생성
+export const ToggleContainer = styled.div<{ justifyContent?: 'space-between' | 'flex-end' }>`
+  display: flex;
+  align-items: center;
+  justify-content: ${props => props.justifyContent || 'space-between'};
+`;
+
+export const ToggleSwitch = styled.div<{ active: boolean; disabled?: boolean }>`
+  // 공통 토글 스위치 스타일
+`;

그리고 각 파일에서 import하여 사용:

-export const ToggleContainer = styled.div`...`;
-export const ToggleSwitch = styled.div<{ active: boolean }>`...`;
+import { ToggleContainer, ToggleSwitch } from '../common/Toggle/Toggle.styled';

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/recordwrite/VoiceToggleSection.styled.ts between lines 9 and
61, the toggle-related styled components are duplicated from
PageRangeSection.styled.ts. To improve maintainability, extract the common
toggle components (ToggleContainer, LeftSection, InfoIcon, ToggleSwitch,
ToggleSlider) into a separate shared styled components file. Then, update both
VoiceToggleSection.styled.ts and PageRangeSection.styled.ts to import these
components from the new shared file instead of redefining them.

Comment on lines 173 to +186
case 'page':
return sortedRecords.filter(record => record.recordType === 'page');
if (selectedPageRange) {
// 페이지 범위가 선택된 경우, 해당 범위 내의 기록만 필터링
return sortedRecords.filter(record => {
if (record.recordType === 'page' && record.pageRange) {
const recordPage = parseInt(record.pageRange);
return recordPage >= selectedPageRange.start && recordPage <= selectedPageRange.end;
}
return false;
});
} else {
// 페이지 범위가 선택되지 않은 경우 모든 페이지 기록 표시
return sortedRecords.filter(record => record.recordType === 'page');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

parseInt 실패 시 처리가 필요합니다.

record.pageRange가 유효하지 않은 문자열일 경우 parseInt가 NaN을 반환하여 의도하지 않은 필터링이 발생할 수 있습니다.

 if (record.recordType === 'page' && record.pageRange) {
   const recordPage = parseInt(record.pageRange);
+  if (isNaN(recordPage)) {
+    return false;
+  }
   return recordPage >= selectedPageRange.start && recordPage <= selectedPageRange.end;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'page':
return sortedRecords.filter(record => record.recordType === 'page');
if (selectedPageRange) {
// 페이지 범위가 선택된 경우, 해당 범위 내의 기록만 필터링
return sortedRecords.filter(record => {
if (record.recordType === 'page' && record.pageRange) {
const recordPage = parseInt(record.pageRange);
return recordPage >= selectedPageRange.start && recordPage <= selectedPageRange.end;
}
return false;
});
} else {
// 페이지 범위가 선택되지 않은 경우 모든 페이지 기록 표시
return sortedRecords.filter(record => record.recordType === 'page');
}
case 'page':
if (selectedPageRange) {
// 페이지 범위가 선택된 경우, 해당 범위 내의 기록만 필터링
return sortedRecords.filter(record => {
if (record.recordType === 'page' && record.pageRange) {
const recordPage = parseInt(record.pageRange);
if (isNaN(recordPage)) {
return false;
}
return recordPage >= selectedPageRange.start && recordPage <= selectedPageRange.end;
}
return false;
});
} else {
// 페이지 범위가 선택되지 않은 경우 모든 페이지 기록 표시
return sortedRecords.filter(record => record.recordType === 'page');
}
🤖 Prompt for AI Agents
In src/pages/memory/Memory.tsx around lines 173 to 186, the use of parseInt on
record.pageRange can return NaN if the string is invalid, causing incorrect
filtering. Update the code to check if the result of parseInt is a valid number
before comparing it with selectedPageRange.start and selectedPageRange.end. If
parseInt returns NaN, exclude that record from the filtered results to prevent
unintended behavior.

Comment on lines +29 to +85
const handleCompleteClick = async () => {
if (isSubmitting) return; // 중복 실행 방지

setIsSubmitting(true);

try {
// 페이지 범위 결정: 입력값이 없으면 마지막 기록 페이지 사용
const finalPageRange = isOverallEnabled
? undefined
: pageRange.trim() !== ''
? pageRange
: lastRecordedPage.toString();

// 투표 옵션 생성
const pollOptionsData = pollOptions
.filter(option => option.trim() !== '')
.map((option, index) => ({
id: `${index + 1}.`,
text: option.trim(),
percentage: 0,
isHighest: false,
}));

// 새 투표 기록 객체 생성
const newPollRecord: Record = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // 고유한 ID
user: '내가', // TODO: 실제 사용자 이름으로 변경
userPoints: 0, // TODO: 실제 사용자 포인트로 변경
content: pollContent,
likeCount: 0,
commentCount: 0,
timeAgo: '방금 전',
createdAt: new Date(),
type: 'poll',
recordType: isOverallEnabled ? 'overall' : 'page',
pageRange: finalPageRange, // 최종 페이지 범위 저장
pollOptions: pollOptionsData,
};

console.log('투표 생성 완료', newPollRecord);
console.log('페이지 범위:', isOverallEnabled ? '전체범위' : `${finalPageRange}p`);
console.log('투표 내용:', pollContent);
console.log('투표 옵션:', pollOptionsData);
console.log('총평 설정:', isOverallEnabled);

// TODO: API 호출하여 서버에 투표 저장
// await api.createPoll(newPollRecord);

navigate('/memory', {
state: { newRecord: newPollRecord },
replace: true,
});
} catch (error) {
console.error('투표 저장 실패:', error);
setIsSubmitting(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

보안 및 UX 개선이 필요합니다.

폼 제출 로직에 몇 가지 개선점이 있습니다.

개선 사항:

+ // 입력값 검증 및 정제
+ const sanitizeInput = (input: string) => input.trim().replace(/[<>]/g, '');
+
 const handleCompleteClick = async () => {
   if (isSubmitting) return;
+  
+  // 입력값 검증 강화
+  const sanitizedContent = sanitizeInput(pollContent);
+  if (!sanitizedContent) {
+    // 사용자에게 오류 메시지 표시 로직 추가
+    return;
+  }

   setIsSubmitting(true);

   try {
+    // 더 안전한 ID 생성
+    const uniqueId = `poll_${Date.now()}_${crypto.randomUUID?.() || Math.random().toString(36).substr(2, 9)}`;
+
     const newPollRecord: Record = {
-      id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
+      id: uniqueId,
       // ...
-      content: pollContent,
+      content: sanitizedContent,
     };

   } catch (error) {
     console.error('투표 저장 실패:', error);
+    // 사용자에게 오류 메시지 표시 (예: 토스트, 스낵바 등)
     setIsSubmitting(false);
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleCompleteClick = async () => {
if (isSubmitting) return; // 중복 실행 방지
setIsSubmitting(true);
try {
// 페이지 범위 결정: 입력값이 없으면 마지막 기록 페이지 사용
const finalPageRange = isOverallEnabled
? undefined
: pageRange.trim() !== ''
? pageRange
: lastRecordedPage.toString();
// 투표 옵션 생성
const pollOptionsData = pollOptions
.filter(option => option.trim() !== '')
.map((option, index) => ({
id: `${index + 1}.`,
text: option.trim(),
percentage: 0,
isHighest: false,
}));
// 새 투표 기록 객체 생성
const newPollRecord: Record = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // 고유한 ID
user: '내가', // TODO: 실제 사용자 이름으로 변경
userPoints: 0, // TODO: 실제 사용자 포인트로 변경
content: pollContent,
likeCount: 0,
commentCount: 0,
timeAgo: '방금 전',
createdAt: new Date(),
type: 'poll',
recordType: isOverallEnabled ? 'overall' : 'page',
pageRange: finalPageRange, // 최종 페이지 범위 저장
pollOptions: pollOptionsData,
};
console.log('투표 생성 완료', newPollRecord);
console.log('페이지 범위:', isOverallEnabled ? '전체범위' : `${finalPageRange}p`);
console.log('투표 내용:', pollContent);
console.log('투표 옵션:', pollOptionsData);
console.log('총평 설정:', isOverallEnabled);
// TODO: API 호출하여 서버에 투표 저장
// await api.createPoll(newPollRecord);
navigate('/memory', {
state: { newRecord: newPollRecord },
replace: true,
});
} catch (error) {
console.error('투표 저장 실패:', error);
setIsSubmitting(false);
}
};
// 입력값 검증 및 정제
const sanitizeInput = (input: string) => input.trim().replace(/[<>]/g, '');
const handleCompleteClick = async () => {
if (isSubmitting) return; // 중복 실행 방지
// 입력값 검증 강화
const sanitizedContent = sanitizeInput(pollContent);
if (!sanitizedContent) {
// TODO: 사용자에게 오류 메시지 표시 로직 추가
return;
}
setIsSubmitting(true);
try {
// 더 안전한 ID 생성
const uniqueId = `poll_${Date.now()}_${crypto.randomUUID?.() || Math.random().toString(36).substr(2, 9)}`;
// 페이지 범위 결정: 입력값이 없으면 마지막 기록 페이지 사용
const finalPageRange = isOverallEnabled
? undefined
: pageRange.trim() !== ''
? pageRange
: lastRecordedPage.toString();
// 투표 옵션 생성
const pollOptionsData = pollOptions
.filter(option => option.trim() !== '')
.map((option, index) => ({
id: `${index + 1}.`,
text: option.trim(),
percentage: 0,
isHighest: false,
}));
// 새 투표 기록 객체 생성
const newPollRecord: Record = {
id: uniqueId,
user: '내가', // TODO: 실제 사용자 이름으로 변경
userPoints: 0, // TODO: 실제 사용자 포인트로 변경
content: sanitizedContent,
likeCount: 0,
commentCount: 0,
timeAgo: '방금 전',
createdAt: new Date(),
type: 'poll',
recordType: isOverallEnabled ? 'overall' : 'page',
pageRange: finalPageRange, // 최종 페이지 범위 저장
pollOptions: pollOptionsData,
};
console.log('투표 생성 완료', newPollRecord);
console.log('페이지 범위:', isOverallEnabled ? '전체범위' : `${finalPageRange}p`);
console.log('투표 내용:', pollContent);
console.log('투표 옵션:', pollOptionsData);
console.log('총평 설정:', isOverallEnabled);
// TODO: API 호출하여 서버에 투표 저장
// await api.createPoll(newPollRecord);
navigate('/memory', {
state: { newRecord: newPollRecord },
replace: true,
});
} catch (error) {
console.error('투표 저장 실패:', error);
// 사용자에게 오류 메시지 표시 (예: 토스트, 스낵바 등)
setIsSubmitting(false);
}
};
🤖 Prompt for AI Agents
In src/pages/pollwrite/PollWrite.tsx around lines 29 to 85, the form submission
logic lacks validation and proper error handling, which can lead to poor user
experience and potential security issues. Add input validation to ensure
pollContent and pollOptions are not empty before proceeding. Disable the submit
button while submitting to prevent multiple submissions. Also, ensure
setIsSubmitting(false) is called in a finally block to reset the submitting
state regardless of success or failure. Remove or secure console.log statements
before production to avoid exposing sensitive data.

Comment on lines +4 to +15
export const Container = styled.div`
display: flex;
flex-direction: column;
background-color: ${semanticColors.background.primary};
min-width: 360px;
max-width: 767px;
min-height: 100vh;
margin: 0 auto;
padding: 96px 20px 100px 20px;
box-sizing: border-box;
gap: 32px;
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

코드 중복 문제가 있습니다.

PollWrite.styled.ts와 동일한 Container 컴포넌트가 정의되어 있습니다. 두 파일에서 완전히 같은 스타일링을 사용하고 있어 코드 중복이 발생했습니다.

공통 스타일 컴포넌트를 생성하여 중복을 제거하는 것을 권장합니다:

// src/components/common/PageContainer.styled.ts 생성
+import styled from '@emotion/styled';
+import { semanticColors } from '../../styles/global/global';
+
+export const PageContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  background-color: ${semanticColors.background.primary};
+  min-width: 360px;
+  max-width: 767px;
+  min-height: 100vh;
+  margin: 0 auto;
+  padding: 96px 20px 100px 20px;
+  box-sizing: border-box;
+  gap: 32px;
+`;

그 후 두 파일에서 이를 import하여 사용하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const Container = styled.div`
display: flex;
flex-direction: column;
background-color: ${semanticColors.background.primary};
min-width: 360px;
max-width: 767px;
min-height: 100vh;
margin: 0 auto;
padding: 96px 20px 100px 20px;
box-sizing: border-box;
gap: 32px;
`;
// src/components/common/PageContainer.styled.ts
import styled from '@emotion/styled';
import { semanticColors } from '../../styles/global/global';
export const PageContainer = styled.div`
display: flex;
flex-direction: column;
background-color: ${semanticColors.background.primary};
min-width: 360px;
max-width: 767px;
min-height: 100vh;
margin: 0 auto;
padding: 96px 20px 100px 20px;
box-sizing: border-box;
gap: 32px;
`;
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.styled.ts lines 4 to 15, the Container
styled component duplicates the exact same styles as in PollWrite.styled.ts,
causing code redundancy. To fix this, create a shared styled component with
these common styles in a separate file, then import and use that shared
component in both RecordWrite.styled.ts and PollWrite.styled.ts to eliminate
duplication.


// 새 기록 객체 생성
const newRecord: Record = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // 고유한 ID

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ID 생성 방식 개선 권장

현재 Date.now()Math.random()을 조합한 ID 생성 방식은 충돌 가능성이 있습니다. 더 안전한 방법을 고려해보세요.

다음과 같은 개선 방안을 권장합니다:

-        id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // 고유한 ID
+        id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, // 고유한 ID

또는 crypto API를 사용하는 것을 고려해보세요:

id: crypto.randomUUID(), // 브라우저 지원이 필요한 경우
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx at line 43, the current ID generation
using Date.now() and Math.random() can lead to collisions. Replace this with a
more reliable method such as using the crypto API's randomUUID() function to
generate unique IDs, ensuring browser compatibility is considered. Update the id
assignment to use crypto.randomUUID() for safer and more robust unique ID
generation.

Comment on lines +68 to +71
} catch (error) {
console.error('기록 저장 실패:', error);
setIsSubmitting(false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 강화 필요

현재 에러 처리가 콘솔 로그와 상태 리셋에만 의존하고 있습니다. 사용자에게 에러 메시지를 표시하는 것이 좋겠습니다.

에러 상태와 사용자 알림을 추가하는 것을 권장합니다:

+  const [error, setError] = useState<string | null>(null);

   } catch (error) {
     console.error('기록 저장 실패:', error);
+    setError('기록 저장에 실패했습니다. 다시 시도해주세요.');
     setIsSubmitting(false);
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 68 to 71, the error
handling only logs the error to the console and resets the submitting state.
Enhance this by adding an error state variable to the component and update it in
the catch block. Then, display a user-friendly error message in the UI based on
this error state to inform users when the save operation fails.

@ho0010 ho0010 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

Comment on lines +47 to +62
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;

// 숫자만 입력 허용
if (value === '' || /^\d+$/.test(value)) {
onPageRangeChange(value);

// 전체 페이지 수를 초과하면 에러 상태로 변경
if (value !== '') {
const page = parseInt(value);
setHasError(page > totalPages);
} else {
setHasError(false);
}
}
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

핸들러에서 검증은 분리하고 로직만 담으면 좋을 것 같아요!

Comment on lines +56 to +59
console.log('기록 작성 완료', newRecord);
console.log('페이지 범위:', isOverallEnabled ? '전체범위' : `${finalPageRange}p`);
console.log('내용:', content);
console.log('총평 설정:', isOverallEnabled);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이 console은 확인차 사용하신 걸까요 아니면 의도하신 걸까요??

Comment on lines +68 to +69
} catch (error) {
console.error('기록 저장 실패:', error);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

전반적으로 에러들이 이런식으로 콘솔로 되어있는데 이런 것들도 확인차 넣어두신 걸까요??

@ljh130334 ljh130334 merged commit 4b958d7 into develop Jul 31, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/recordwrite branch August 10, 2025 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants