Skip to content

Conversation

@hyesngy
Copy link
Member

@hyesngy hyesngy commented Dec 6, 2025

Summary

관련 있는 Issue를 태그해주세요. (e.g. > - #100)

Tasks

  • 노트 저장 json 구조 수정

To Reviewer

이제 단어카드와 빈칸카드 포함해서도 저장이랑 불러오기 잘 됨

Summary by CodeRabbit

릴리스 노트

  • 개선 사항

    • 노트 로드/저장 시 콘텐츠 형식 변환 안정성 및 처리 신뢰성 향상
    • 중첩된 콘텐츠 처리와 기본 텍스트 대체 동작 개선
  • 새로운 기능

    • 백엔드 형식 ↔ 에디터 형식 간 자동 변환 지원 추가
    • 노트 콘텐츠 속성에 선택적 '역순 표시' 옵션 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@hyesngy hyesngy self-assigned this Dec 6, 2025
@hyesngy hyesngy requested a review from a team as a code owner December 6, 2025 08:28
@hyesngy hyesngy added the 👾 Fix 버그 수정 관련 label Dec 6, 2025
@hyesngy hyesngy linked an issue Dec 6, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Dec 6, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
cardify Ready Ready Preview Comment Dec 6, 2025 8:47am

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Walkthrough

노트 편집기 컨텍스트에 TipTap JSON과 백엔드 API 노트 JSON 간의 변환기를 추가하고, 노트 로드 시 API→TipTap으로, 저장 시 TipTap→API로 변환을 통합했습니다. 또한 NoteContentAttrs에 선택적 reversed?: boolean 필드를 추가했습니다.

Changes

Cohort / File(s) 변경 요약
편집기 컨텍스트 및 변환 통합
src/contexts/note-editor-context.tsx
노트 로드 시 transformContentFromApi로 백엔드 포맷을 TipTap 호환 JSON으로 변환해 initialContent로 설정. 저장 시 TipTap JSON을 transformContentForApi로 변환해 request.contents에 할당. 일부 디버깅 로그 정리(주석 처리된 로그 제거, 주요 로그 유지).
변환 유틸리티
src/utils/note-content-transformer.ts
transformContentFromApi(content: NoteContent): NoteContenttransformContentForApi(content: NoteContent): NoteContent 추가. vocacardblankcard 유형을 처리하고 중첩 콘텐츠를 재귀적으로 변환. 내부 텍스트 추출용 extractTextFromContent 헬퍼 포함.
타입 확장
src/types/note/note-request.ts
NoteContentAttrs 인터페이스에 선택적 부울 필드 reversed?: boolean 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Editor as TipTap Editor
  participant Context as NoteEditorContext
  participant Transformer as note-content-transformer
  participant API as Backend API

  Editor->>Context: 요청된 노트 로드 (note data)
  Context->>Transformer: transformContentFromApi(apiNote.contents)
  Transformer-->>Context: TipTap 호환 JSON
  Context-->>Editor: 초기 콘텐츠 설정 (initialContent)

  Editor->>Context: 사용자 편집 후 저장 요청 (TipTap JSON)
  Context->>Transformer: transformContentForApi(tipTapJson)
  Transformer-->>Context: 백엔드 포맷(contents)
  Context->>API: 저장 요청 (request.contents)
  API-->>Context: 저장 결과
  Context-->>Editor: 저장 완료/오류 상태
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • 주의할 파일 / 영역:
    • src/utils/note-content-transformer.ts: vocacard/blankcard 변환 로직(속성 보존, 기본값 처리, 재귀 처리)의 정확성 및 경계 케이스 검증
    • src/contexts/note-editor-context.tsx: 변환 적용 시 타이밍(로드/저장)과 기존 로깅/오류 처리 유지 여부
    • src/types/note/note-request.ts: reversed 필드가 사용처에서 예상대로 처리되는지(undef vs false 차이) 확인

Possibly related issues

  • [Fix]: 노트 저장 json 구조 수정 #115: 변환기를 도입하고 저장/로드에서 TipTap↔백엔드 JSON 변환을 적용하여, 해당 이슈의 “잘못된 노트 저장 JSON 구조” 문제와 직접적으로 관련됩니다.

시 (Poem)

🐰✨ API와 TipTap 사이를 뛰노는 토끼예요,
앞면과 뒷면을 살짝 뒤집어 보이고,
블랭크는 채우고, 텍스트는 모아,
작은 헬퍼로 큰 다리 놓았죠.
킁킁—변환은 완성! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 PR의 주요 변경사항을 명확하게 반영하고 있으며, 노트 저장 JSON 구조 수정이라는 핵심 변경을 잘 요약하고 있습니다.
Description check ✅ Passed PR 설명이 제공된 템플릿의 필수 섹션(Summary, Tasks)을 포함하고 있으며, 관련 이슈 태그(#115)와 작업 내용, 리뷰어를 위한 추가 정보도 포함되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#115

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Dec 6, 2025

✨ Storybook 확인: 🔗 https://6785fc0fb0ff9f8d21d80624-jzykomjnto.chromatic.com/

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/contexts/note-editor-context.tsx (2)

19-75: 변환 로직 확인 및 에러 핸들링 고려 필요

백엔드 형식을 TipTap 형식으로 변환하는 로직이 전반적으로 잘 구현되어 있습니다. 특히 line 27에서 reversed 속성을 ?? false로 안전하게 처리한 점이 좋습니다.

다만 예상치 못한 데이터 구조가 들어올 경우를 대비한 에러 핸들링을 추가하는 것을 권장합니다. 특히 백엔드 API 응답의 형식이 변경되거나 손상된 데이터가 전달될 경우 앱이 크래시될 수 있습니다.

선택적 개선사항: 기본값 문자열들("질문을 입력하세요", "답변을 입력하세요" 등)을 상수로 추출하면 유지보수성이 향상됩니다.


78-126: blankcard의 attrs 처리 일관성 확인 필요

전반적인 변환 로직은 잘 구현되어 있습니다.

vocacard의 경우 line 90에서 ...content.attrs를 사용해 reversed를 포함한 모든 속성을 보존하는 반면, blankcard(lines 107-114)는 question_front, question_back, answer만 명시적으로 설정합니다.

현재는 blankcard가 추가 attrs를 사용하지 않아 문제가 없지만, 향후 blankcard에 attrs가 추가될 경우 데이터 손실이 발생할 수 있습니다. 의도적인 설계인지 확인해주세요.

추가로, vocacard와 동일한 에러 핸들링 개선 권장사항이 적용됩니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74eac70 and 99c8f60.

📒 Files selected for processing (2)
  • src/contexts/note-editor-context.tsx (3 hunks)
  • src/types/note/note-request.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/types/note/note-request.ts (3)
src/pages/note-editor/components/tiptap-node/voca-card-node/voca-card-node-extension.ts (3)
  • addAttributes (28-38)
  • attributes (33-35)
  • state (84-115)
src/pages/note-editor/components/tiptap-node/voca-card-node/answer-node.tsx (1)
  • updateReversed (28-49)
src/pages/note-editor/components/tiptap-node/voca-card-node/voca-card-node.tsx (1)
  • reversed (5-15)
src/contexts/note-editor-context.tsx (1)
src/types/note/note-request.ts (2)
  • NoteContent (45-51)
  • WriteNoteRequest (53-58)
🔇 Additional comments (4)
src/types/note/note-request.ts (1)

27-38: LGTM! reversed 필드 추가가 적절합니다.

vocacard의 뒤집기 상태를 추적하기 위한 reversed?: boolean 필드 추가가 관련 코드(voca-card-node-extension.ts)의 구현과 일관성 있게 잘 되어 있습니다.

src/contexts/note-editor-context.tsx (3)

7-16: 텍스트 추출 로직이 깔끔합니다.

재귀적으로 텍스트를 추출하는 로직이 잘 구현되어 있습니다. 옵셔널 체이닝으로 undefined 케이스도 적절히 처리되고 있습니다.


181-184: 로드 시 변환 통합이 적절합니다.

백엔드 형식을 TipTap 형식으로 변환한 후 initialContent로 설정하는 흐름이 올바르게 구현되어 있습니다. 디버그 로깅도 개발 시 유용합니다.


206-214: 저장 시 변환 통합이 올바릅니다.

TipTap JSON을 백엔드 API 형식으로 변환한 후 전송하는 로직이 정확하게 구현되어 있습니다. 이를 통해 vocacard의 reversed 속성을 포함한 모든 데이터가 올바르게 저장될 것입니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99c8f60 and 88ec84b.

📒 Files selected for processing (2)
  • src/contexts/note-editor-context.tsx (3 hunks)
  • src/utils/note-content-transformer.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/contexts/note-editor-context.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/utils/note-content-transformer.ts (1)
src/types/note/note-request.ts (1)
  • NoteContent (45-51)
🔇 Additional comments (2)
src/utils/note-content-transformer.ts (2)

4-13: 헬퍼 함수 로직이 올바릅니다.

텍스트 노드에서 직접 텍스트를 추출하고, 중첩된 콘텐츠를 재귀적으로 처리하는 로직이 정확합니다. 빈 콘텐츠에 대한 조기 반환과 빈 문자열 폴백 처리도 적절합니다.


16-71: API에서 TipTap 형식으로의 변환 로직이 올바릅니다.

양방향 변환 로직에서 vocacard와 blankcard 타입을 모두 적절하게 처리하고 있으며, 재귀적 변환도 정확합니다. 기본값 처리와 옵셔널 체이닝 사용이 적절합니다.

Comment on lines +74 to +121
export const transformContentForApi = (content: NoteContent): NoteContent => {
// vocacard 변환
if (content.type === "vocacard") {
const questionNode = content.content?.find((c) => c.type === "question");
const answerNode = content.content?.find((c) => c.type === "answer");

const questionText = extractTextFromContent(questionNode?.content);
const answerText = extractTextFromContent(answerNode?.content);

return {
type: "vocacard",
attrs: {
...content.attrs,
question_front: questionText,
answer: [answerText],
},
};
}

// blankcard 변환
if (content.type === "blankcard") {
const prefixNode = content.content?.find((c) => c.type === "prefix");
const blankNode = content.content?.find((c) => c.type === "blank");
const suffixNode = content.content?.find((c) => c.type === "suffix");

const prefixText = extractTextFromContent(prefixNode?.content);
const blankText = extractTextFromContent(blankNode?.content);
const suffixText = extractTextFromContent(suffixNode?.content);

return {
type: "blankcard",
attrs: {
question_front: prefixText,
question_back: suffixText,
answer: [blankText],
},
};
}

if (content.content) {
return {
...content,
content: content.content.map(transformContentForApi),
};
}

return content;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

vocacard와 blankcard 간 attrs 처리 불일치를 수정하세요.

vocacard 변환(86번 줄)에서는 ...content.attrs로 기존 속성을 보존하지만, blankcard 변환(105-109번 줄)에서는 기존 속성을 보존하지 않습니다. 이로 인해 blankcard의 추가 속성(예: reversed 또는 향후 추가될 필드)이 손실될 수 있습니다.

다음 diff를 적용하여 blankcard도 기존 attrs를 보존하도록 수정하세요:

   return {
     type: "blankcard",
     attrs: {
+      ...content.attrs,
       question_front: prefixText,
       question_back: suffixText,
       answer: [blankText],
     },
   };
📝 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 transformContentForApi = (content: NoteContent): NoteContent => {
// vocacard 변환
if (content.type === "vocacard") {
const questionNode = content.content?.find((c) => c.type === "question");
const answerNode = content.content?.find((c) => c.type === "answer");
const questionText = extractTextFromContent(questionNode?.content);
const answerText = extractTextFromContent(answerNode?.content);
return {
type: "vocacard",
attrs: {
...content.attrs,
question_front: questionText,
answer: [answerText],
},
};
}
// blankcard 변환
if (content.type === "blankcard") {
const prefixNode = content.content?.find((c) => c.type === "prefix");
const blankNode = content.content?.find((c) => c.type === "blank");
const suffixNode = content.content?.find((c) => c.type === "suffix");
const prefixText = extractTextFromContent(prefixNode?.content);
const blankText = extractTextFromContent(blankNode?.content);
const suffixText = extractTextFromContent(suffixNode?.content);
return {
type: "blankcard",
attrs: {
question_front: prefixText,
question_back: suffixText,
answer: [blankText],
},
};
}
if (content.content) {
return {
...content,
content: content.content.map(transformContentForApi),
};
}
return content;
};
export const transformContentForApi = (content: NoteContent): NoteContent => {
// vocacard 변환
if (content.type === "vocacard") {
const questionNode = content.content?.find((c) => c.type === "question");
const answerNode = content.content?.find((c) => c.type === "answer");
const questionText = extractTextFromContent(questionNode?.content);
const answerText = extractTextFromContent(answerNode?.content);
return {
type: "vocacard",
attrs: {
...content.attrs,
question_front: questionText,
answer: [answerText],
},
};
}
// blankcard 변환
if (content.type === "blankcard") {
const prefixNode = content.content?.find((c) => c.type === "prefix");
const blankNode = content.content?.find((c) => c.type === "blank");
const suffixNode = content.content?.find((c) => c.type === "suffix");
const prefixText = extractTextFromContent(prefixNode?.content);
const blankText = extractTextFromContent(blankNode?.content);
const suffixText = extractTextFromContent(suffixNode?.content);
return {
type: "blankcard",
attrs: {
...content.attrs,
question_front: prefixText,
question_back: suffixText,
answer: [blankText],
},
};
}
if (content.content) {
return {
...content,
content: content.content.map(transformContentForApi),
};
}
return content;
};
🤖 Prompt for AI Agents
In src/utils/note-content-transformer.ts around lines 74 to 121, the blankcard
branch returns attrs without preserving existing content.attrs (unlike
vocacard), which can drop fields like reversed; modify the blankcard return to
spread existing attrs into attrs (e.g. attrs: { ...content.attrs,
question_front: prefixText, question_back: suffixText, answer: [blankText] }) so
all prior attributes are retained while still setting the transformed fields.

@hyesngy hyesngy merged commit fa2f95d into develop Dec 6, 2025
7 checks passed
@hyesngy hyesngy deleted the fix/#115 branch December 6, 2025 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👾 Fix 버그 수정 관련

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix]: 노트 저장 json 구조 수정

2 participants