Conversation
- PUT /api/clips/:clipId 엔드포인트 추가 - 소유자 검증 로직 포함 (user_id 기반 보안 체크) - 태그 처리: 기존 태그 재사용 및 새 태그 자동 생성 - 유효성 검사: clipId, userId, 수정 필드 검증 - 부분 필드 수정 지원 (title, url, tagName, memo) - 3-layer 아키텍처: Repository→Service→Controller - 단위 테스트: Repository(5개), Service(12개), Controller(11개) - Swagger 문서 스펙 완전 준수
📝 WalkthroughWalkthrough클립 업데이트 기능을 추가. PUT /api/clips/:clipId 라우트를 신설해 인증 사용자만 제목/URL/메모/태그를 수정 가능. 컨트롤러-서비스-레포지토리 계층을 구현하고 Supabase를 통해 소유자 조건으로 업데이트. 표준 성공/에러 응답 처리 및 세부 단위 테스트 추가. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant R as Router (/api/clips/:clipId)
participant C as Controller (handleUpdateClip)
participant S as Service (updateClip)
participant TR as Tag Repo (findTagByName/createTag)
participant CR as Clip Repo (updateClipById)
participant DB as Supabase (clips/tags)
U->>R: PUT /api/clips/:clipId {title,url,tagName,memo}
R->>C: req(params, user, body)
C->>C: 인증 확인 (user.id)
alt 인증 실패
C-->>U: 401 에러 응답
else 인증 성공
C->>S: updateClip(clipId, userId, fields)
S->>S: 입력 검증/필드 정제
opt tagName 제공됨
S->>TR: findTagByName(userId, tagName)
alt 태그 존재
TR-->>S: tag {id}
else 미존재
S->>TR: createTag(userId, tagName)
TR-->>S: tag {id}
end
end
S->>CR: updateClipById(clipId, userId, updateData{title,url,memo,tag_id?})
CR->>DB: update clips where id & user_id
DB-->>CR: updated row or error
alt 성공
CR-->>S: updated clip
S-->>C: {message, id, updatedFields}
C-->>U: 200 성공 응답
else 에러
CR-->>S: throw CustomError(404/500)
S-->>C: throw
C-->>U: status(from error) 에러 응답
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
Summary of ChangesHello @Dobbymin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 Swagger 문서 스펙에 따라 클립 내용을 수정하는 API를 구현합니다. 사용자가 자신의 클립(제목, URL, 태그, 메모)을 안전하게 업데이트할 수 있도록 하며, 3-layer 아키텍처를 적용하여 모듈화된 코드와 강력한 보안 검증을 제공합니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
클립 내용 수정 API 구현을 위한 PR 잘 보았습니다. 전반적으로 3-layer 아키텍처를 잘 따르고 있으며, 컨트롤러, 서비스, 리포지토리 각 계층의 역할 분리가 명확합니다. 특히 DB 레벨에서 소유자 검증을 구현하여 보안을 강화한 점과, PostgREST 에러 코드를 분석하여 구체적인 에러 처리를 한 점이 인상적입니다. 테스트 코드 또한 각 계층별로 꼼꼼하게 작성되어 코드의 안정성을 높여줍니다. 한 가지 아쉬운 점은 서비스 로직에서 필드 값을 빈 문자열로 업데이트할 수 없는 작은 버그가 있어, 이 부분에 대한 수정을 제안합니다. 자세한 내용은 코드 리뷰 댓글을 참고해주세요. 훌륭한 작업입니다!
| const updateData = {}; | ||
| if (filteredFields.title) updateData.title = filteredFields.title; | ||
| if (filteredFields.url) updateData.url = filteredFields.url; | ||
| if (filteredFields.memo) updateData.memo = filteredFields.memo; | ||
| if (tagId) updateData.tag_id = tagId; |
There was a problem hiding this comment.
현재 updateData를 구성하는 로직은 if (filteredFields.title)과 같이 falsy 값을 확인하고 있어, title 등의 필드를 빈 문자열("")로 수정하려는 경우 의도치 않게 누락될 수 있습니다. 예를 들어, 사용자가 클립의 제목을 지우고 싶어 빈 문자열을 보내면, 이 변경사항이 DB에 반영되지 않습니다.
filteredFields에 프로퍼티가 존재하는지 여부로 확인하도록 수정하면 이 문제를 해결할 수 있습니다. in 연산자를 사용하는 것을 추천합니다.
| const updateData = {}; | |
| if (filteredFields.title) updateData.title = filteredFields.title; | |
| if (filteredFields.url) updateData.url = filteredFields.url; | |
| if (filteredFields.memo) updateData.memo = filteredFields.memo; | |
| if (tagId) updateData.tag_id = tagId; | |
| const updateData = {}; | |
| if ('title' in filteredFields) updateData.title = filteredFields.title; | |
| if ('url' in filteredFields) updateData.url = filteredFields.url; | |
| if ('memo' in filteredFields) updateData.memo = filteredFields.memo; | |
| if (tagId) updateData.tag_id = tagId; |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/apis/clip/controller/handleUpdateClip.js (1)
20-23: 컨트롤러에서 undefined 필드 제외(선택)로 페이로드 최소화서비스가 필터링을 수행하긴 하지만, 컨트롤러에서 undefined를 사전에 제거하면 불필요한 키 전달을 줄일 수 있습니다. 테스트가 현재 undefined 전달을 기대하고 있으므로 적용 시 테스트 갱신이 필요합니다.
다음과 같이 updateFields를 구성하는 방식을 고려해 보세요:
- const { title, url, tagName, memo } = req.body; - const updateFields = { title, url, tagName, memo }; + const { title, url, tagName, memo } = req.body; + const updateFields = Object.fromEntries( + Object.entries({ title, url, tagName, memo }).filter(([, v]) => v !== undefined) + );tests/apis/clip/service/updateClip.test.js (1)
150-174: 빈 문자열 시나리오 테스트 보강 제안서비스 로직 결정(A안: 무시 / B안: 비우기 허용)에 따라 다음 케이스를 추가해 주세요.
- 입력이 ' ' 등 공백뿐인 경우: trim 후 처리 기대값(A안이면 NO_VALID_UPDATE_DATA 400, B안이면 해당 필드가 ''로 업데이트).
- tagName이 공백뿐인 경우: 태그 업데이트 미수행(A안) 또는 tag_id 미변경(B안).
- updatedFields가 실제 업데이트된 필드와 일치하는지 검증.
원하시면 제가 테스트 케이스 패치를 만들어 드리겠습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/apis/clip/controller/handleUpdateClip.js(1 hunks)src/apis/clip/repository/updateClipById.js(1 hunks)src/apis/clip/service/updateClip.js(1 hunks)src/routes/router.js(2 hunks)tests/apis/clip/controller/handleUpdateClip.test.js(1 hunks)tests/apis/clip/repository/updateClipById.test.js(1 hunks)tests/apis/clip/service/updateClip.test.js(1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
{app.js,src/**/*.js}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
변수명과 함수명은 영어로, 주석은 한국어로 작성한다.
Files:
src/routes/router.jssrc/apis/clip/repository/updateClipById.jssrc/apis/clip/service/updateClip.jssrc/apis/clip/controller/handleUpdateClip.js
src/routes/router.js
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
라우팅은 src/routes/router.js에서 관리하고 경로는 /api/{도메인}/{액션} 형식을 따른다.
Files:
src/routes/router.js
src/apis/**/repository/*.js
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
리포지토리는 데이터베이스 접근 로직을 담당하며 파일명은 find{Entity}.js 형태를 따른다 (예: findProfileByUserId.js).
Files:
src/apis/clip/repository/updateClipById.js
src/apis/**/service/*.js
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
서비스는 비즈니스 로직을 구현하며 파일명은 {verb}{Noun}.js 형태를 따른다 (예: loginUser.js, createUser.js).
Files:
src/apis/clip/service/updateClip.js
src/apis/**/controller/*.js
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/apis/**/controller/*.js: 컨트롤러는 요청 데이터를 추출하고 서비스 호출 후 표준 응답 포맷으로 응답하며 try/catch로 에러를 처리한다.
컨트롤러 파일명은 handle{Action}.js 형태를 따른다 (예: handleUserLogin.js).
모든 API 응답은 src/utils/responseFormatter.js의 createSuccessResponse/createErrorResponse를 통해 표준 포맷으로 반환한다.
Files:
src/apis/clip/controller/handleUpdateClip.js
🧬 Code graph analysis (7)
src/routes/router.js (1)
src/apis/clip/controller/handleUpdateClip.js (2)
handleUpdateClip(9-36)handleUpdateClip(9-36)
src/apis/clip/repository/updateClipById.js (3)
src/apis/clip/controller/handleUpdateClip.js (1)
userId(12-12)src/apis/clip/service/updateClip.js (1)
updateData(64-64)src/utils/errors.js (1)
CustomError(1-13)
src/apis/clip/service/updateClip.js (3)
src/apis/clip/controller/handleUpdateClip.js (2)
userId(12-12)updateFields(22-22)src/utils/errors.js (1)
CustomError(1-13)src/apis/clip/repository/updateClipById.js (2)
updateClipById(16-33)updateClipById(16-33)
tests/apis/clip/controller/handleUpdateClip.test.js (3)
src/apis/clip/service/updateClip.js (2)
updateClip(18-78)updateClip(18-78)src/apis/clip/controller/handleUpdateClip.js (2)
handleUpdateClip(9-36)handleUpdateClip(9-36)src/utils/errors.js (1)
CustomError(1-13)
src/apis/clip/controller/handleUpdateClip.js (1)
src/apis/clip/service/updateClip.js (2)
updateClip(18-78)updateClip(18-78)
tests/apis/clip/service/updateClip.test.js (3)
src/apis/clip/repository/updateClipById.js (2)
updateClipById(16-33)updateClipById(16-33)src/apis/clip/service/updateClip.js (4)
updateClip(18-78)updateClip(18-78)existingTag(52-52)newTag(58-58)src/utils/errors.js (1)
CustomError(1-13)
tests/apis/clip/repository/updateClipById.test.js (2)
src/apis/clip/repository/updateClipById.js (3)
supabase(17-23)updateClipById(16-33)updateClipById(16-33)src/utils/errors.js (1)
CustomError(1-13)
🔇 Additional comments (4)
src/routes/router.js (1)
10-10: PUT /api/clips/:clipId 라우팅 연동 LGTM핸들러 임포트 및 PUT 라우트 연결이 현재 Clip API의 REST 스타일과 일관됩니다. 권한은 전역 conditionalAuth로 강제되고, 상세조회만 optionalAuth를 사용해 정책도 일관적입니다.
업데이트 라우트가 상세 조회(GET /api/clips/:clipId)보다 먼저 선언되어도 충돌은 없지만, 미들웨어 적용 순서/정책을 다시 한 번 확인해 주세요(특히 authExcludePaths가 /api/auth로만 한정되는 점). 필요 시 제가 간단한 라우팅 점검 스크립트를 드릴 수 있습니다.
Also applies to: 30-30
tests/apis/clip/repository/updateClipById.test.js (1)
32-51: 레포지토리 성공 플로우 검증 LGTM체인 호출(eq → select → single)과 반환 데이터 검증이 명확합니다. PGRST116/기타 에러 케이스도 분리 테스트되어 신뢰도가 높습니다.
tests/apis/clip/controller/handleUpdateClip.test.js (1)
38-62: 컨트롤러 성공 케이스 커버리지 충분, 표준 응답 포맷 사용 확인서비스 호출 파라미터 구성, 성공 응답 포맷, 200 상태코드 검증이 명확합니다. 부분 업데이트/태그 포함 케이스도 별도 검증되어 좋습니다.
src/apis/clip/service/updateClip.js (1)
38-42: 빈 문자열('') 처리에 따른 updatedFields와 DB 업데이트 불일치
trim 후 빈 문자열은filteredFields에 포함되나
if (filteredFields.x)truthy 체크로updateData에 반영되지 않아
updatedFields반환값과 실제 DB 업데이트 결과가 달라질 수 있습니다.
– 모든updateData매핑은 truthy 체크 대신
Object.prototype.hasOwnProperty.call(filteredFields, '필드명')사용 권장
A) 빈 문자열 무시: 필터링 단계에normalized !== ''조건 추가
B) 빈 문자열 허용(필드 비우기 지원): 필터링 단계 유지, 매핑만 개선위 두 안 중 의도하신 동작을 선택해 주세요.
| * @param {string} [updateData.title] 클립 제목 | ||
| * @param {string} [updateData.url] 클립 URL | ||
| * @param {string} [updateData.memo] 클립 메모 | ||
| * @param {number} [updateData.tagId] 태그 ID |
There was a problem hiding this comment.
JSDoc 필드명 불일치: tagId → tag_id
서비스/DB 컬럼과 일치하도록 JSDoc을 정정하세요. 현재 코드는 tag_id를 사용합니다.
다음 주석 변경을 제안합니다:
- * @param {number} [updateData.tagId] 태그 ID
+ * @param {number} [updateData.tag_id] 태그 ID📝 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.
| * @param {number} [updateData.tagId] 태그 ID | |
| * @param {number} [updateData.tag_id] 태그 ID |
🤖 Prompt for AI Agents
In src/apis/clip/repository/updateClipById.js around line 12, the JSDoc
parameter name is currently "tagId" but the code/DB uses "tag_id"; update the
JSDoc to use "tag_id" to match the service/DB field. Edit the @param line to
replace "tagId" with "tag_id" and keep the rest of the description intact so the
documentation accurately reflects the actual parameter name used by the
implementation.
📝 요약 (Summary)
Swagger 문서 스펙에 따라 클립 내용 수정 API를 3-layer 아키텍처로 구현하였습니다. 사용자가 특정 클립 ID를 통해 자신의 클립 내용(제목, URL, 태그, 메모)을 안전하게 수정할 수 있는 기능을 제공합니다.
✅ 주요 변경 사항 (Key Changes)
• PUT /api/clips/:clipId 엔드포인트 추가
• 3-layer 아키텍처 완전 구현 (Repository → Service → Controller)
• 소유자 검증 로직 포함 (user_id 기반 보안 체크)
• 태그 처리: 기존 태그 재사용 및 새 태그 자동 생성
• 부분 필드 수정 지원 (title, url, tagName, memo)
• 체계적인 에러 처리 (404, 400, 500 상태 코드)
• 유효성 검사 및 데이터 정제 (공백 제거, null/undefined 필터링)
• 단위 테스트 작성 (Repository/Service/Controller 각 계층)
💻 상세 구현 내용 (Implementation Details)
🏗️ Repository Layer (
updateClipById.js)• Supabase를 활용한 DB 클립 수정 쿼리 구현
• 소유자 검증을 위한 user_id 조건 포함
• 수정된 클립의 기본 정보 (id, title, url, memo, tag_id) 반환
• PGRST116 에러 코드를 통한 404 처리
🔧 Service Layer (
updateClip.js)• clipId, userId 유효성 검사 (숫자, 양수 확인)
• 수정 필드 유효성 검사 및 데이터 정제
• 태그 처리 로직: 기존 태그 재사용 또는 새 태그 자동 생성
• 부분 필드 수정 지원 (일부 필드만 수정 가능)
• 성공 시 수정된 필드 목록 포함한 사용자 친화적 메시지 반환
🌐 Controller Layer (
handleUpdateClip.js)• Express req.params에서 clipId 추출
• req.user에서 사용자 인증 정보 확인
• req.body에서 수정 필드들 추출 (title, url, tagName, memo)
• 표준화된 성공/에러 응답 포맷 사용
• HTTP 상태 코드 자동 매핑 (200, 401, 400, 404, 500)
🛣️ Router 연동
• Express PUT 메서드 라우트 등록
• 기존 인증 미들웨어와 연동
• RESTful API 패턴 준수
🚀 트러블 슈팅 (Trouble Shooting)
1. 태그 처리 로직 설계
문제: 클립 수정 시 새로운 태그명이 제공될 때, 기존 태그를 재사용할지 새로 생성할지 결정 필요
해결: 먼저 기존 태그를 조회하고, 없으면 새로 생성하는 2단계 로직으로 구현
2. 부분 필드 수정 지원
문제: 모든 필드를 필수로 받을지, 제공된 필드만 수정할지 결정 필요
해결: 유효한 필드만 필터링하여 부분 수정을 지원하도록 구현
3. 소유자 검증 보안
문제: 클립 수정 시 다른 사용자의 클립을 수정하는 보안 취약점 방지 필요
해결: Repository 레벨에서 user_id 조건을 포함하여 DB 차원에서 소유권 검증
테스트 실행 이슈
• 모든 단위 테스트가 성공적으로 통과
• Repository(5개), Service(12개), Controller(11개) 총 28개 테스트 케이스
• 로컬 서버에서 정상적으로 동작 확인됨
성능 고려사항
• 태그 처리 시 2번의 DB 쿼리 발생 가능 (기존 태그 조회 + 새 태그 생성)
• 추후 태그 캐싱 또는 배치 처리 고려 가능
API 동작 특징
• 수정할 데이터가 제공되지 않으면 400 에러 반환
• 존재하지 않는 클립 또는 타인 소유 클립 수정 시 404 에러
• 태그명만 제공된 경우에도 정상 처리 (기존 태그 재사용 또는 새 태그 생성)
📸 스크린샷 (Screenshots)
Swagger API 문서:
• PUT /api/clips/{clipId} 엔드포인트 정상 표시
• 200, 400, 404 응답 스키마 정의 완료
• 요청 본문 스키마 정의: title, url, tagName, memo 필드
로컬 서버 실행:
테스트 실행 결과:
#️⃣ 관련 이슈 (Related Issues)
• feat#31-update-clip 브랜치로 개발 진행
• Swagger 문서 스펙 기반 구현
• 클립 삭제 API(#28)와 일관된 아키텍처 적용
🧪 테스트 커버리지
Repository Layer 테스트
• ✅ 소유자 검증 포함 정상 클립 수정 성공
• ✅ 존재하지 않는 클립 또는 타인 소유 클립 404 에러
• ✅ 데이터베이스 에러 500 처리
• ✅ 올바른 Supabase 쿼리 호출 검증
• ✅ 부분 데이터 수정 처리
Service Layer 테스트
• ✅ 유효한 clipId, userId로 성공 처리
• ✅ 기존 태그 재사용 로직 검증
• ✅ 새로운 태그 자동 생성 로직 검증
• ✅ 부분 필드 수정 지원 확인
• ✅ 유효성 검사 (clipId, userId, updateData)
• ✅ 데이터 정제 (공백 제거, null 필터링)
• ✅ Repository 에러 전파 확인
• ✅ 경계값 테스트 (유효하지 않은 필드, 빈 데이터)
Controller Layer 테스트
• ✅ HTTP 요청/응답 처리
• ✅ params에서 clipId 추출
• ✅ req.user에서 사용자 ID 추출
• ✅ req.body에서 수정 필드들 추출
• ✅ 성공/에러 응답 포맷팅
• ✅ HTTP 상태 코드 자동 매핑
• ✅ 인증 실패 401 에러 처리
• ✅ 400/404/500 에러 시나리오
🤖 Copilot 리뷰 가이드라인
리뷰어 및 Copilot에게 요청사항:
handleUpdateClip.js,updateClip.js) 확인.js확장자 포함 여부 확인특별히 검토해주세요:
추가 리뷰 포인트:
• 🔍 태그 처리 로직의 효율성 검토 (기존 태그 재사용 vs 새 태그 생성)
• 🔍 부분 필드 수정 로직의 완전성 검토
• 🔍 데이터 유효성 검사 및 정제 로직 검토
• 🔍 소유자 검증의 보안성 및 완전성 검토
Summary by CodeRabbit
신기능
테스트