-
Notifications
You must be signed in to change notification settings - Fork 1
문서 수정 Conflict 기능 구현 #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/#134
Are you sure you want to change the base?
문서 수정 Conflict 기능 구현 #140
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 드디어 이 기능이 구현되었군요 ㅋㅋㅋ관련해서 궁금한 점은 코멘트 달아두었습니다!!
고생하셨어요~~~!
router.push(`${URLS.wiki}/${uuid}`); | ||
router.refresh(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
postDocument
와 putDocument
에서 요청이 성공한 경우 라우팅을 하도록 유도하고 있어서 코드가 중복될 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오~ 감사합니다. 전혀 모르고 있었네요..!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<Modal> | ||
<h2 className="mb-4 text-2xl font-bold">문서 충돌 해결</h2> | ||
<p className="mb-4">다른 사용자가 문서를 수정했습니다. 아래 내용을 병합하여 저장해주세요.</p> | ||
<div className="mb-4"> | ||
<TuiEditor initialValue={initialContent} onChange={setContent} /> | ||
</div> | ||
<div className="flex justify-end gap-2"> | ||
<Button style="tertiary" size="m" onClick={() => modal.close()}> | ||
취소 | ||
</Button> | ||
<Button style="primary" size="m" onClick={handleResolve} disabled={!isResolved}> | ||
충돌 해결 완료 | ||
</Button> | ||
</div> | ||
</Modal>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
컴포넌트 부분을 따로 분리하면 좋을 것 같아요!~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다~ 분리해봤어요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (newLatest && conflict.version !== newLatest.latestVersion) { | ||
conflictModal.closeWithReject(); | ||
alert('병합하는 동안 새로운 변경사항이 생겼습니다. 다시 충돌을 해결해주세요.'); | ||
const conflictText = createConflictText(newLatest.contents, resolvedContent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
충돌이 여러 곳에서 발생해도 conflictText는 모든 충돌을 하나로 묶어 한 번만 생성되는 걸까요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 맞아요.. 이게 좀 아쉽긴 하네요 여러 곳에서 잡을 수 있으면 더 좋을 것 같은데
이건 시간이 남으면 더 시도해볼게요!
그래도 충돌은 잡을 수 있으니!
const handleResolve = async (resolvedContent: string) => { | ||
try { | ||
// 재충돌 방지 | ||
const newLatest = await getDocumentByUUIDClient(uuid); | ||
|
||
// 충돌 시 새 버전과 새로 불러온 버전이 다르다면 다시 충돌상황 | ||
if (newLatest && conflict.version !== newLatest.latestVersion) { | ||
conflictModal.closeWithReject(); | ||
alert('병합하는 동안 새로운 변경사항이 생겼습니다. 다시 충돌을 해결해주세요.'); | ||
const conflictText = createConflictText(newLatest.contents, resolvedContent); | ||
setConflict({version: newLatest.latestVersion, content: conflictText}); | ||
conflictModal.open(); | ||
return; | ||
} | ||
|
||
await handleSubmit(resolvedContent); | ||
conflictModal.close(true); | ||
} catch (error) { | ||
console.error(error); | ||
alert('저장에 실패했습니다.'); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 핸들러의 위치는 65번 라인보다 아래쪽에 위치하면 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 그 부분이 더 좋았는데, 이게.. 이거 때문에 위에 적었어요..
Block-scoped variable 'handleResolve' used before its declaration.ts(2448)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 근본적인 원인을 해결했어요. PostHeader에 너무 많은 기능이 있었는데 충돌 기능때문에 더 많아져서, 충돌 관련 로직을 useConflictModal로 분리했습니다. 확실히 더 보기 좋은 것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다👍
|
||
const onSubmit = async () => { | ||
if (mode === 'edit') { | ||
await handleConflictCheck(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 연산을 수행할 때 로딩이 추가되면 좋을 것 같네용 문서의 길이가 길면 시간이 오래 걸릴 수도 있을 것 같아서요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋아요~ 반영해봤습니다.
d48b6bf
issue
모달이 필요해서 feature/#134 브랜치를 rebase하고 그 위에 작업을 쌓았습니다. 134 브랜치를 먼저 머지해야합니다.
구현 사항
문서 수정 시에 내가 수정하던 도중 다른 사람이 문서를 수정했을 때 덮어씌워지는 문제가 있었습니다.
이 문제를 개선하고자 문서 충돌 기능을 구현했습니다.
문서 충돌 판단 기준
문서 수정하기 페이지에 진입했을 때 서버에서 최신의 문서를 가져옵니다 (최신 버전을 기준으로 수정해야하므로)
이 때 현재 문서의 최신 버전 latestVersion을 가져오도록 기능이 추가되었습니다. 이를 사용해서 판단합니다.
사용자가 문서를 수정하고 버튼을 누를 때, 페이지 진입 시 가져왔던 버전과 버튼을 누를 시점의 버전이 다르다면 내가 수정하던 도중 다른 사람이 문서를 수정한 경우입니다.
이 때 충돌 모달을 띄워서 사용자에게 충돌을 직접 해결하도록 했습니다. 충돌 표시는 git과 유사하게 했습니다.
(크루위키를 사용하는 유저들은 모두 개발자임으로 git 충돌 해결하는 방법은 알겠죠?ㅋㅋㅋㅋㅋㅋ)
문서 충돌 해결방법
git merge나 rebase conflict의 경우 current, incoming 두 가지 정보를 보여줍니다.
비슷하게 incoming을 최신 버전, current를 내 작업으로 처리해서 사용자에게 보여줍니다.
구분을 <<<<<와 ------으로 했으며 이 문구가 모두 지워지면 충돌 해결입니다.
TuiEditor onChange 외부에서 넣어줄 수 있도록 개선
문서 충돌 해결 모달 내에서도 TuiEditor를 사용합니다.
이전에 TuiEditor를 수정하기, 작성하기 페이지에서만 사용했기 때문에 내부에서 zustand로 onChange를 받아와서 컴포넌트 내에서 수정해주었습니다. 그러나 이제 이 두 페이지 외 모달에서도 TuiEditor를 사용해야했기 때문에 onChange를 외부에서 받아와서 사용할 수 있도록 개선했습니다. (이 방향성이 맞긴하지)
시연 영상
참고로 dev 환경 캐시 업데이트 문제로 수정 완료 시 바로 최신 데이터가 보이지 않아요.
1. 정상 수정
2025-10-06.20-29-26.mp4
2. 문서 충돌 상황
2025-10-06.20-29-57.mp4
3. 문서 재충돌 상황
2025-10-06.20-30-31.mp4
🫡 참고사항
아래는 제가 기능을 구현하면서 작성한 기능명세서입니다.
초안을 작성하고 AI에게 검토 및 보완을 부탁했는데 퀄리티가 꽤 좋은 것 같아서 공유합니다.
이 내용을 읽으면 더 이해가 잘 될거에요.
문서 충돌 해결 기능 명세서
1. 개요
사용자가 문서를 편집하고 저장할 때, 다른 사용자가 먼저 동일한 문서를 수정하여 발생할 수 있는 데이터 유실(덮어쓰기) 문제를 방지합니다. 충돌이 감지되면 사용자에게 두 버전의 차이점을 보여주는 UI를 제공하고, 사용자가 직접 내용을 병합하여 안전하게 저장할 수 있도록 돕는 기능을 구현합니다.
2. 사용자 시나리오
2.1. 충돌이 발생하는 경우
<<<<<
,>>>>>
)와 함께 표시됩니다.2.2. 충돌이 발생하지 않는 경우
3. 기능 명세
3.1. 버전 충돌 감지
originalVersion
)**을 클라이언트 상태로 저장하고 있어야 합니다.GET /document/{uuid}/log
API를 호출하여 문서의 최신 로그 정보를 가져옵니다.latestVersion
)을 확인합니다.originalVersion
과 서버에서 받은latestVersion
을 비교합니다.originalVersion !== latestVersion
이면 충돌로 간주하고, 3.2의 '충돌 해결 UI'를 실행합니다.originalVersion === latestVersion
이면 정상 저장 로직을 수행합니다.3.2. 충돌 해결 UI (모달)
CreatePortal
)을 사용하여document.body
의 최상단에 렌더링합니다.<<<<< 최신 버전
~=====
구간: 배경색 (e.g.,rgba(255, 0, 0, 0.1)
)=====
~>>>>> 내 작업
구간: 배경색 (e.g.,rgba(0, 0, 255, 0.1)
)disabled
).<<<<<
,=====
,>>>>>
패턴의 문자열이 모두 사라졌을 때 활성화됩니다. (실시간으로 편집기 내용을 파싱하여 상태를 업데이트해야 합니다.)3.3. 충돌 내용 비교 알고리즘
diff-match-patch
또는jsdiff
와 같은 검증된 라이브러리 사용을 권장합니다. 이 라이브러리들은 두 텍스트를 비교하여 변경된 부분을 상세히 알려주므로, 이를 가공하여<<<<<
형식의 결과물을 쉽게 만들 수 있습니다.remoteContent
)을 추가로 가져옵니다. (GET /document/{uuid}
)localContent
)과remoteContent
를 Diff 라이브러리에 전달합니다.3.4. 충돌 해결 및 저장
POST /document/{uuid}
)를 호출합니다.GET /document/{uuid}/log
를 다시 호출하여latestVersion
이 이전 단계(3.1)에서 확인한latestVersion
과 동일한지 한 번 더 확인하는 로직을 추가하는 것을 강력히 권장합니다. 만약 또 충돌했다면, 사용자에게 "병합하는 동안 새로운 변경사항이 생겼습니다. 페이지를 새로고침하여 다시 시도해주세요." 와 같은 메시지를 보여주고 프로세스를 처음부터 다시 시작하도록 유도합니다.4. 테스트 케이스
4.1. 정상 케이스
<<<<<
등)가 하나라도 남아있으면 '충돌 해결 완료' 버튼이 비활성화 상태를 유지한다.4.2. 예외 케이스
/document/{uuid}/log
API 호출에 실패하면, 사용자에게 "최신 버전을 확인하는데 실패했습니다. 잠시 후 다시 시도해주세요." 와 같은 에러 메시지를 표시한다.GET /document/{uuid}
) 호출에 실패하면, "최신 내용을 가져오는데 실패했습니다." 에러 메시지를 표시한다.4.3. 경계 케이스