Skip to content

Comments

[FEATURE] 릴리즈 이전 QA 사항 반영#140

Merged
sunhwaaRj merged 21 commits intodevelopfrom
feature/#136-QA
Jan 11, 2026
Merged

[FEATURE] 릴리즈 이전 QA 사항 반영#140
sunhwaaRj merged 21 commits intodevelopfrom
feature/#136-QA

Conversation

@sunhwaaRj
Copy link
Contributor

@sunhwaaRj sunhwaaRj commented Jan 10, 2026

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📌 관련 이슈번호


✅ Key Changes

image

노션 '진행중 태그' 모두 완료.


📸 스크린샷 or 실행영상


🎸 기타 사항 or 추가 코멘트

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 키워드 영역에 공유 버튼 추가 및 모바일 전용 렌더링 개선
    • 텍스트 잘림 시 툴팁 제공하는 훅 도입으로 태그/키워드 상호작용 개선
  • 버그 수정
    • 빈 단락을 줄바꿈으로 보존해 콘텐츠 표시 안정화
    • 지원 완료 모달 닫을 때 관련 데이터 갱신(캐시 무효화) 보장
  • 개선 사항
    • 로딩·빈 상태 UI 개선, 헤더·레이아웃 시각·포지셔닝 조정 및 특정 오버레이 동작 변경
    • 로그인·하이드레이션 흐름 및 공유 모드 처리 안정화

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

@sunhwaaRj sunhwaaRj requested a review from woneeeee January 10, 2026 15:10
@sunhwaaRj sunhwaaRj self-assigned this Jan 10, 2026
@sunhwaaRj sunhwaaRj added the FIX 버그 수정 label Jan 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

Walkthrough

useGetProject에 refetch 옵션이 추가/전달되고, 콘텐츠 빈 단락을 <p><br /></p>로 대체해 sanitize; 키워드 축약·툴팁 훅 도입 및 관련 컴포넌트 적용; ApplyCompleteModal에 postId 전달 후 모달 종료 시 쿼리 무효화 추가; 라우팅·레이아웃·헤더·오버레이 및 소규모 UI 조정이 적용되었습니다.

Changes

Cohort / File(s) 요약
프로젝트 정보
src/app/(main)/project/[id]/_components/ProjectInfo.tsx, src/app/(main)/project/[id]/applicants/_components/ProjectInfo.tsx
콘텐츠의 빈 <p></p><p><br /></p>로 교체해 sanitize 처리; 메인 ProjectInfo에서 isWriter 기반 레이아웃 조건 추가; useGetProject 호출에 refetch 옵션 전달(메인: true).
쿼리 훅
src/hooks/queries/useProject.ts
useGetProject 시그니처에 두번째 인자(p0: { refetchOnMount, refetchOnWindowFocus }) 추가하고 해당 옵션을 useQuery에 스프레드하도록 변경.
에디터(편집)
src/app/(main)/project/[id]/edit/_components/FormEditor.tsx
useGetProject 호출에 { refetchOnMount: false, refetchOnWindowFocus: false } 전달(편집 화면에서 리패치 비활성).
모달·캐시 관리
src/constants/ModalList.ts, src/components/modal/ApplyCompleteModal.tsx, src/components/modal/apply/ApplyModal.tsx, src/components/modal/apply/ApplyModalMo.tsx
ApplyCompleteModalProps에 postId?: number 추가; ApplyModal/ApplyModalMo에서 성공 시 openModal('applyComplete', { postId })로 전달; ApplyCompleteModal이 닫힐 때 queryClient.invalidateQueries(['project', postId]) 호출하도록 변경.
키워드 UI 및 페이지 흐름
src/app/(main)/teampsylog/_components/KeywordBar.tsx, .../KeywordItem.tsx, .../KeywordPage.tsx, .../KeywordGuideOverlay.tsx, head/[uuid]/_components/KeywordCard.tsx, .../LogNote.tsx, page.tsx
KeywordItem에 isMobileDevice prop 추가 및 useTruncatedTooltip으로 모바일 축약·툴팁 적용(공백 제외 5자); KeywordBar에 공유 버튼 추가; KeywordPage에 하이드레이션·로딩 가드 및 로컬스토리지 기반 가이드 토글; Guide 오버레이에 h-dvh 추가; KeywordCard에 로딩/빈상태 분기; 레이아웃 클래스 일부 조정.
프로필 관련
src/components/profile/Profile1.tsx, src/components/profile/ProfileTag.tsx
Profile1: Linkrouter.push()로 내비게이션 전환 및 경계색 조정; ProfileTag: useTruncatedTooltip 도입으로 truncation·툴팁 이벤트·포지셔닝 처리(구조·이벤트 변경).
헤더·오버레이 래퍼
src/components/common/MobileHeader.tsx, src/components/layout/MobileOverlayWrapper.tsx
MobileHeader에 relative z-30 및 뒤로가기 버튼 cursor-pointer 추가; MobileOverlayWrapper에서 /teampsylog/head/:uuid 경로용 오버레이 제거.
유틸 훅 추가
src/hooks/useTruncatedTooltip.ts
공백 제외 문자 기준 truncation, 툴팁 표시 상태·포지셔닝·이벤트 핸들러를 제공하는 새 훅 추가(모바일 터치 자동 숨김 포함).
기타 소규모 변경
src/components/recruit/ProcessMethod.tsx, src/components/login/LoginSocialList.tsx
ControllerdefaultValue="ONLINE" 추가; 소셜 로그인에서 window.location.replace 사용으로 히스토리 대체.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ApplyModal
    participant ModalSystem as ApplyCompleteModal
    participant QueryClient
    participant Cache

    User->>ApplyModal: 지원 폼 제출
    ApplyModal->>ModalSystem: openModal('applyComplete', { postId })
    User->>ModalSystem: 모달 닫기 클릭
    ModalSystem->>QueryClient: invalidateQueries(['project', postId])
    QueryClient->>Cache: 해당 쿼리 항목 무효화
    ModalSystem->>User: 모달 종료
Loading
sequenceDiagram
    participant User
    participant KeywordPage
    participant localStorage
    participant KeywordGuideOverlay

    KeywordPage->>localStorage: 'hideKeywordGuide' 조회
    alt 로컬에 표시 기록 없음
        localStorage-->>KeywordPage: null
        KeywordPage->>KeywordGuideOverlay: 가이드 표시
        KeywordGuideOverlay->>User: 가이드 오버레이 표시
    else 이미 숨김으로 설정됨
        localStorage-->>KeywordPage: 'true'
        KeywordPage->>User: 가이드 없이 페이지 렌더
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • woneeeee
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR 설명은 PR 유형, 관련 이슈 번호, Key Changes 섹션을 포함하고 있으나 각 변경사항에 대한 상세 설명이 부족합니다. Key Changes 섹션에서 스크린샷 외에 구체적인 변경사항들(예: 로그인 페이지 뒤로가기, 타이핑 처리, 모바일 레이아웃 등)에 대한 문자 설명을 추가하면 더 명확합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 QA 사항 반영이라는 주요 변경사항을 명확하게 설명하고 있어 릴리즈 전 버그 및 기능 개선 작업의 범위를 잘 나타냅니다.
Linked Issues check ✅ Passed 코드 변경사항들이 #136 이슈의 QA 피드백 반영이라는 목표에 부합하며, 로그인 리다이렉트 보호, 모바일 레이아웃 개선, 콘텐츠 정제, 쿼리 최적화 등 다양한 UI/UX 및 기능적 개선이 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 QA 개선 범위 내에 있으며, 로그인 페이지 UX, 모바일 레이아웃, 데이터 정제, 쿼리 최적화 등이 일관되게 릴리즈 준비 목표에 부합합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e769a7 and 69b90f0.

📒 Files selected for processing (1)
  • src/components/login/LoginSocialList.tsx
🔇 Additional comments (1)
src/components/login/LoginSocialList.tsx (1)

12-12: OAuth 리다이렉트 시 히스토리 교체 방식으로 변경 - LGTM!

세 OAuth 제공자(카카오, 구글, 네이버) 모두에서 window.location.replace()로 일관되게 변경하여 로그인 페이지로의 뒤로가기를 방지합니다. OAuth 플로우에서 사용자 혼란을 줄이는 적절한 UX 패턴입니다.

Also applies to: 21-21, 31-31


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.

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(main)/teampsylog/_components/KeywordPage.tsx (1)

72-84: 중복된 useEffect가 있습니다.

Lines 72-77과 Lines 79-84에 동일한 로직의 useEffect가 두 번 선언되어 있습니다. 이로 인해 setShowGuide(true)가 불필요하게 두 번 호출될 수 있습니다.

🔧 중복 제거 제안
  useEffect(() => {
    if (share) return;

    const hide = localStorage.getItem('hideKeywordGuide');
    if (!hide) setShowGuide(true);
  }, [share]);

- useEffect(() => {
-   if (!share) {
-     const hide = localStorage.getItem('hideKeywordGuide');
-     if (!hide) setShowGuide(true);
-   }
- }, [share]);
🤖 Fix all issues with AI agents
In @src/app/(main)/project/[id]/_components/ProjectInfo.tsx:
- Around line 10-14: Compute and validate the numeric id before passing it to
useGetProject and avoid passing NaN: e.g. const postId = Number(id); call
useGetProject({ postId: Number.isNaN(postId) ? undefined : postId }, {
refetchOnMount: true, refetchOnWindowFocus: true }); this ensures
getSingleProject is not invoked with NaN (adjust the undefined fallback to match
useGetProject's expected type if it expects null or a specific sentinel).

In @src/app/(main)/teampsylog/_components/KeywordItem.tsx:
- Around line 65-72: The tooltip only appears on touch (handleTouch) and lacks
keyboard/mouse support and ARIA associations; make the wrapper element focusable
(add tabIndex=0) and add onMouseEnter/onMouseLeave and onFocus/onBlur handlers
(in addition to handleTouch) to set showTooltip, give the tooltip an id and
role="tooltip" and reference it from the wrapper via aria-describedby (or
provide an accessible aria-label fallback using keyword/displayText), and ensure
keyboard users can toggle/see the tooltip the same way as mouse/touch users by
reusing the same state-updating logic used by handleTouch.
- Around line 43-48: handleTouch uses setTimeout to call setShowTooltip but
doesn't clear the timer, risking setState after unmount; store the timeout id
(e.g., via a ref like tooltipTimeoutRef), call clearTimeout on that id before
scheduling a new timeout, and add a useEffect cleanup that clears
tooltipTimeoutRef.current on unmount so that setShowTooltip is never invoked
after the component unmounts; keep existing guards
(isEditMode/isMobileDevice/isTruncated) and ensure the ref is reset to null
after clearing.

In @src/components/modal/ApplyCompleteModal.tsx:
- Around line 8-18: The modal only invalidates queries inside handleClose, so
other close paths (BaseModal's onClose, overlay/ESC, "지원한 프로젝트 보러가기" click) skip
refetch and postId may be undefined; change the component to provide a unified
close handler that always runs before calling the passed onClose: use a single
wrapper function (e.g., handleClose) passed into BaseModal's onClose and used by
all internal close buttons, and inside it guard the invalidate call with if
(postId != null) then queryClient.invalidateQueries({ queryKey: ['project',
postId] }) before calling onClose(); update any other internal close handlers to
call this same handleClose so invalidation is consistent.

In @src/components/profile/ProfileTag.tsx:
- Around line 45-55: The touch handler starts a setTimeout but never clears it,
which can cause state updates after unmount; modify handleTouch to save the
timeout id (e.g., use a ref like tooltipTimeoutRef) when calling setTimeout,
clear any existing timeout before setting a new one, and add a useEffect cleanup
that calls clearTimeout on that ref in the component unmount; ensure references
to setShowTooltip and setTooltipPos still occur inside the timeout and that the
ref is used to cancel the pending callback.

In @src/components/recruit/ProcessMethod.tsx:
- Around line 22-25: The render function inside the Controller is causing a side
effect by calling onChange when value is undefined; remove that mutation from
render and instead set the initial value via Controller's defaultValue or the
form's defaultValues in the useForm call. Specifically, stop invoking onChange
in the render callback that destructures field: { value, onChange } and set
progressWay to ONLINE_VALUE (or DURATION_OPTIONS[1].value) via
Controller(defaultValue=...) or in useForm<RecruitFormType>({ defaultValues: {
progressWay: ... } }) so the field starts with the intended default without
triggering changes during render.
🧹 Nitpick comments (7)
src/components/profile/Profile1.tsx (1)

100-105: 데스크톱과 모바일 간 네비게이션 동작 불일치를 확인해주세요.

현재 구현에서:

  • 데스크톱 (line 70): window.open(..., '_blank')로 새 탭에서 열림
  • 모바일 (lines 100-105): router.push()로 같은 탭에서 이동

이러한 동작 차이가 의도된 UX인지 확인이 필요합니다. 또한, router.push() 대신 Next.js Link 컴포넌트를 사용하면 자동 프리페칭과 최적화를 활용할 수 있습니다.

♻️ Link 컴포넌트를 활용한 개선안

동작을 유지하면서 Next.js 최적화를 활용하려면:

  <div className="body-7 flex border-t border-gray-300 text-gray-800">
    <button className="flex-1 py-3" onClick={handleOpenDetailModal}>
      상세보기
    </button>
    <div className="w-[1px] bg-gray-300"></div>
-   <button
-     className="flex-1 py-3 text-center"
-     onClick={() => router.push(`/teampsylog/${data.uuid}`)}
-   >
-     전체 키워드 보기
-   </button>
+   <Link
+     href={`/teampsylog/${data.uuid}`}
+     className="body-7 flex-1 py-3 text-center text-gray-800"
+   >
+     전체 키워드 보기
+   </Link>
  </div>

이 방식을 사용하면:

  • Next.js의 자동 프리페칭 활용
  • 더 나은 페이지 전환 성능
  • 표준 앵커 태그 접근성 향상

만약 router.push()가 특정 이유로 필요한 경우라면 현재 구현을 유지해도 됩니다.

src/components/recruit/ProcessMethod.tsx (1)

13-13: 불필요한 옵셔널 체이닝.

DURATION_OPTIONS는 상수 배열이므로 DURATION_OPTIONS[1]?.value 대신 DURATION_OPTIONS[1].value를 사용해도 안전합니다.

♻️ 제안된 개선 사항
-const ONLINE_VALUE = DURATION_OPTIONS[1]?.value;
+const ONLINE_VALUE = DURATION_OPTIONS[1].value;
src/components/profile/ProfileTag.tsx (1)

68-86: 툴팁 위치 계산 방식이 일관성이 없습니다.

position: absolutegetBoundingClientRect() + window.scroll* 조합은 부모 요소가 positioned ancestor일 경우 오프셋이 발생할 수 있습니다. React Portal을 사용하거나 position: fixed로 변경하는 것을 권장합니다.

♻️ fixed positioning 사용 제안
      {showTooltip && tooltipPos && tagRef.current && (
        <span
          style={{
-           position: 'absolute',
-           left: tooltipPos.left + tagRef.current.offsetWidth / 2,
-           top: tooltipPos.top + 4,
+           position: 'fixed',
+           left: tagRef.current.getBoundingClientRect().left + tagRef.current.offsetWidth / 2,
+           top: tagRef.current.getBoundingClientRect().bottom + 4,
            transform: 'translateX(-50%)',
            background: 'rgba(60,60,60,0.95)',
src/app/(main)/teampsylog/head/[uuid]/_components/KeywordCard.tsx (1)

20-25: 불필요한 삼항 조건문입니다.

hasKeywords 조건의 양쪽 분기에서 동일한 클래스(items-center justify-center)를 반환하므로 삼항 연산자가 불필요합니다.

♻️ 단순화 제안
    <section
-     className={
-       `relative my-2.5 flex min-h-100 flex-col gap-10 overflow-hidden rounded-2xl bg-gray-100 px-2.5 py-9.5 ` +
-       (hasKeywords ? 'items-center justify-center' : 'items-center justify-center')
-     }
+     className="relative my-2.5 flex min-h-100 flex-col items-center justify-center gap-10 overflow-hidden rounded-2xl bg-gray-100 px-2.5 py-9.5"
    >
src/app/(main)/teampsylog/_components/KeywordItem.tsx (2)

1-2: 선택적 개선: import 문 병합

동일한 모듈에서 가져오는 import 문을 하나로 병합하면 코드가 더 깔끔해집니다.

♻️ 제안하는 수정사항
-import React from 'react';
-import { useState } from 'react';
+import React, { useState } from 'react';

25-41: 성능 최적화: 절단 로직 메모이제이션 필요

키워드 절단 로직이 매 렌더링마다 실행됩니다. keywordisMobileDevice가 변경되지 않았을 때 불필요한 재계산을 방지하기 위해 useMemo를 사용하는 것을 권장합니다.

또한 매직 넘버 5를 상수로 추출하면 유지보수가 용이해집니다.

♻️ 제안하는 수정사항
+const MOBILE_KEYWORD_LIMIT = 5;
+
 const KeywordItem = ({
   keyword,
   isEditMode = false,
   isSelected = false,
   isPlaceholder = false,
   onClick,
   isMobileDevice = false,
 }: KeywordItemProps) => {
   const [showTooltip, setShowTooltip] = useState(false);
 
-  const limit = isMobileDevice ? 5 : 9999;
-  const textLength = getTextLengthWithoutSpaces(keyword);
-  const isTruncated = textLength > limit;
-  let displayText = keyword;
-  if (isMobileDevice && isTruncated) {
-    // 띄어쓰기 제외 5글자 제한
-    let count = 0;
-    displayText = '';
-    for (let i = 0; i < keyword.length; i++) {
-      if (keyword[i] !== ' ') count++;
-      if (count > limit) {
-        displayText += '...';
-        break;
-      }
-      displayText += keyword[i];
-    }
-  }
+  const { displayText, isTruncated } = React.useMemo(() => {
+    const limit = isMobileDevice ? MOBILE_KEYWORD_LIMIT : 9999;
+    const textLength = getTextLengthWithoutSpaces(keyword);
+    const truncated = textLength > limit;
+    
+    if (!isMobileDevice || !truncated) {
+      return { displayText: keyword, isTruncated: truncated };
+    }
+    
+    let count = 0;
+    let text = '';
+    for (let i = 0; i < keyword.length; i++) {
+      if (keyword[i] !== ' ') count++;
+      if (count > limit) {
+        text += '...';
+        break;
+      }
+      text += keyword[i];
+    }
+    
+    return { displayText: text, isTruncated: truncated };
+  }, [keyword, isMobileDevice]);
src/app/(main)/project/[id]/_components/ProjectInfo.tsx (1)

16-17: 빈 문단 치환 정규식이 너무 타이트해서 케이스가 누락될 수 있습니다.
<p> </p>, <p>&nbsp;</p> 같은 “비어 보이는 문단”은 현재 치환되지 않습니다.

제안 diff
-  const sanitizedContent = DOMPurify.sanitize(data.content.replace(/<p><\/p>/g, '<p><br /></p>'));
+  const normalizedContent = data.content.replace(
+    /<p>(?:\s|&nbsp;)*<\/p>/gi,
+    '<p><br /></p>',
+  );
+  const sanitizedContent = DOMPurify.sanitize(normalizedContent);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a99083b and 8f9156b.

📒 Files selected for processing (19)
  • src/app/(main)/project/[id]/_components/ProjectInfo.tsx
  • src/app/(main)/project/[id]/applicants/_components/ProjectInfo.tsx
  • src/app/(main)/teampsylog/_components/KeywordBar.tsx
  • src/app/(main)/teampsylog/_components/KeywordGuideOverlay.tsx
  • src/app/(main)/teampsylog/_components/KeywordItem.tsx
  • src/app/(main)/teampsylog/_components/KeywordPage.tsx
  • src/app/(main)/teampsylog/_components/LogNote.tsx
  • src/app/(main)/teampsylog/head/[uuid]/_components/KeywordCard.tsx
  • src/app/(main)/teampsylog/page.tsx
  • src/components/common/MobileHeader.tsx
  • src/components/layout/MobileOverlayWrapper.tsx
  • src/components/modal/ApplyCompleteModal.tsx
  • src/components/modal/apply/ApplyModal.tsx
  • src/components/modal/apply/ApplyModalMo.tsx
  • src/components/profile/Profile1.tsx
  • src/components/profile/ProfileTag.tsx
  • src/components/recruit/ProcessMethod.tsx
  • src/constants/ModalList.ts
  • src/hooks/queries/useProject.ts
💤 Files with no reviewable changes (1)
  • src/components/layout/MobileOverlayWrapper.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
src/components/recruit/ProcessMethod.tsx (1)
src/constants/Dropdown.ts (1)
  • DURATION_OPTIONS (16-21)
src/components/profile/Profile1.tsx (2)
src/contexts/ModalContext.tsx (1)
  • useModal (55-59)
src/hooks/queries/useProfile.ts (1)
  • useGetProfile (5-10)
src/hooks/queries/useProject.ts (2)
src/types/project.ts (1)
  • ResponseProject (25-35)
src/libs/api/project.ts (1)
  • getSingleProject (13-19)
src/components/profile/ProfileTag.tsx (1)
src/hooks/useIsMobile.ts (1)
  • useIsMobile (5-19)
src/components/modal/ApplyCompleteModal.tsx (1)
src/constants/ModalList.ts (1)
  • ApplyCompleteModalProps (39-42)
src/app/(main)/project/[id]/_components/ProjectInfo.tsx (1)
src/hooks/queries/useProject.ts (1)
  • useGetProject (8-17)
src/app/(main)/teampsylog/head/[uuid]/_components/KeywordCard.tsx (2)
src/hooks/queries/useRequesterInfo.ts (1)
  • useRequesterInfo (6-12)
src/hooks/queries/useKeyword.ts (1)
  • useGetRandomKeywords (73-79)
🔇 Additional comments (16)
src/app/(main)/teampsylog/_components/LogNote.tsx (1)

69-69: 반응형 레이아웃 개선이 적절합니다.

데스크톱에서 왼쪽 패널의 오른쪽 모서리 라운딩을 제거하여 오른쪽 패널(line 137)과 시각적으로 매끄럽게 연결되도록 수정했습니다. 모바일에서는 단일 패널로 양쪽 모서리가 유지되며, 데스크톱에서는 두 패널의 외곽 모서리만 둥글게 처리되어 일체감 있는 디자인을 구현했습니다.

src/components/profile/Profile1.tsx (2)

7-7: LGTM: Next.js App Router 표준 패턴을 올바르게 사용했습니다.

useRouter 훅을 next/navigation에서 가져와서 초기화하는 방식이 Next.js 13+ App Router의 표준 패턴에 부합합니다.

Also applies to: 12-12


22-22: LGTM: 데스크톱과 모바일 간 일관성을 개선했습니다.

테두리 색상을 border-gray-400에서 border-gray-300으로 변경하여 모바일 버전(line 77)과 동일한 스타일을 적용했습니다. 이는 디자인 일관성 향상에 도움이 됩니다.

src/components/common/MobileHeader.tsx (1)

16-20: LGTM!

헤더에 relative z-50을 적용하여 오버레이 컴포넌트들과의 z-index 충돌을 방지하고, 뒤로가기 버튼에 cursor-pointer를 추가하여 클릭 가능함을 명확히 표시한 점이 좋습니다.

src/app/(main)/teampsylog/_components/KeywordGuideOverlay.tsx (1)

50-50: LGTM!

h-dvh 클래스 추가로 모바일 브라우저에서 동적 뷰포트 높이를 올바르게 처리합니다. fixed inset-0과 함께 사용하여 오버레이가 일관되게 전체 화면을 덮도록 합니다.

src/app/(main)/teampsylog/page.tsx (1)

6-9: LGTM!

Fragment 대신 section 요소를 사용하여 시맨틱 구조를 제공하고, h-dvh 클래스로 동적 뷰포트 높이를 적용한 점이 좋습니다. 관련 오버레이 컴포넌트 변경사항과 일관성 있게 적용되었습니다.

src/app/(main)/teampsylog/_components/KeywordBar.tsx (1)

121-136: LGTM!

모바일 뷰에서 공유 버튼 추가와 isMobileDevice prop 전달이 잘 구현되었습니다. 데스크톱과 모바일 간 일관된 기능 제공이 좋습니다.

src/app/(main)/teampsylog/head/[uuid]/_components/KeywordCard.tsx (1)

54-101: LGTM!

로딩 상태와 빈 상태에 대한 조건부 렌더링이 잘 구현되었습니다. 사용자 경험을 위한 적절한 피드백을 제공합니다.

src/app/(main)/teampsylog/_components/KeywordPage.tsx (2)

153-155: LGTM!

!keywordData 조건 추가로 데이터가 없을 때도 로딩 상태를 표시하여 사용자 경험을 개선했습니다.


163-167: LGTM!

공유 모드에서 상단 마진을 조건부로 적용하여 레이아웃을 적절히 조정했습니다.

src/app/(main)/teampsylog/_components/KeywordItem.tsx (1)

4-13: 잘 구현되었습니다!

인터페이스 정의가 명확하고, getTextLengthWithoutSpaces 헬퍼 함수를 컴포넌트 외부에 정의하여 불필요한 재생성을 방지한 점이 좋습니다.

src/components/modal/apply/ApplyModal.tsx (1)

41-45: 성공 후 applyCompletepostId 전달 변경은 의도에 부합
후속 모달에서 캐시 무효화/리패칭을 하려는 흐름이면 이 payload 추가는 좋습니다.

src/components/modal/apply/ApplyModalMo.tsx (1)

68-72: 모바일 플로우도 성공 후 postId 전달로 정합성 확보
ApplyModal(데스크탑)과 동일하게 applyCompletepostId를 넘겨 후속 처리 일관성이 생겼습니다.

src/constants/ModalList.ts (1)

39-42: ApplyCompleteModalPropspostId 추가는 호출부 변경과 일치
옵셔널로 확장한 것도 기존 호출 호환성 측면에서 안전합니다.

src/app/(main)/project/[id]/applicants/_components/ProjectInfo.tsx (1)

58-60: 원본 리뷰 코멘트는 부정확합니다

recruitingPostContent는 타입 정의상 string (비-nullable)이며, 라인 56에서 if (!data) return null;data에 대한 가드가 있어 안전합니다. .replace() 호출 시점에는 data.recruitingPost.recruitingPostContent가 항상 string으로 보장되므로 추가적인 널 가드가 필요하지 않습니다.

Likely an incorrect or invalid review comment.

src/app/(main)/project/[id]/_components/ProjectInfo.tsx (1)

10-13: 작성 완료 후 상태 최신화 목적이라면 refetchOnMount/windowFocus 적용은 합리적입니다. 다만 트래픽 증가 여지는 있으니 의도 확인만 추천합니다.

Also applies to: 20-25

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

🤖 Fix all issues with AI agents
In @src/components/profile/ProfileTag.tsx:
- Around line 77-96: The tooltip uses position: 'absolute' while tooltipPos is
computed in document coordinates (getBoundingClientRect + window.scrollX/Y),
causing mispositioning when an ancestor is positioned; fix by rendering the
tooltip into document.body using a React Portal so document-coordinates are
anchored to the body (use ReactDOM.createPortal to render the span to
document.body) and keep using tooltipPos, tagRef and children to compute
left/top (leave transform: 'translateX(-50%)' to center). Ensure the portal
branch replaces the existing conditional render (the showTooltip && tooltipPos
&& tagRef.current block) so the tooltip is appended to body and no longer
affected by positioned ancestors.
🧹 Nitpick comments (5)
src/components/modal/ApplyCompleteModal.tsx (1)

15-20: 쿼리 무효화 로직 개선 고려

현재 invalidateQueriesawait하고 있는데, 대부분의 경우 쿼리 무효화는 fire-and-forget 패턴으로 처리해도 충분합니다. 모달이 닫히는 동안 백그라운드에서 무효화가 진행될 수 있어 사용자 경험이 더 빠릅니다.

다만, 지원 완료 직후 마이페이지로 이동할 때 최신 데이터가 즉시 필요한 경우라면 현재 구현이 적절할 수 있습니다. 프로젝트의 데이터 동기화 요구사항에 따라 판단하세요.

⚡ 더 빠른 모달 닫기를 위한 선택적 개선안
 const handleClose = async () => {
   if (postId != null) {
-    await queryClient.invalidateQueries({ queryKey: ['project', postId] });
+    queryClient.invalidateQueries({ queryKey: ['project', postId] });
   }
   onClose();
 };
src/components/profile/ProfileTag.tsx (2)

1-3: Import 문을 하나로 통합하세요.

React와 훅들을 여러 줄에 나누어 import하고 있습니다. 가독성을 위해 하나로 통합하는 것이 좋습니다.

♻️ 제안하는 수정
 import { useIsMobile } from '@/hooks/useIsMobile';
-import React, { useEffect } from 'react';
-import { useState, useRef } from 'react';
+import React, { useEffect, useState, useRef } from 'react';

15-29: 중복 코드: 텍스트 자르기 로직을 공유 유틸리티로 추출하세요.

getTextLengthWithoutSpaces 함수와 자르기 로직이 KeywordItem.tsx와 동일하게 구현되어 있습니다. DRY 원칙을 위해 공유 유틸리티 함수로 추출하는 것을 권장합니다.

♻️ 공유 유틸리티 예시
// src/utils/textUtils.ts
export const getTextLengthWithoutSpaces = (text: string) => text.replace(/\s/g, '').length;

export const truncateText = (text: string, limit: number): { displayText: string; isTruncated: boolean } => {
  const isTruncated = getTextLengthWithoutSpaces(text) > limit;
  if (!isTruncated) return { displayText: text, isTruncated: false };
  
  let count = 0;
  let displayText = '';
  for (let i = 0; i < text.length; i++) {
    if (text[i] !== ' ') count++;
    if (count > limit) {
      displayText += '...';
      break;
    }
    displayText += text[i];
  }
  return { displayText, isTruncated: true };
};
src/app/(main)/teampsylog/_components/KeywordItem.tsx (2)

1-2: Import 문을 하나로 통합하세요.

React 관련 import를 여러 줄로 나누지 않고 하나로 통합하는 것이 좋습니다.

♻️ 제안하는 수정
-import React, { useEffect, useRef } from 'react';
-import { useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';

35-35: 매직 넘버 대신 명시적인 값 사용을 고려하세요.

9999는 "자르기 없음"을 의미하지만, Infinity 또는 명명된 상수를 사용하면 의도가 더 명확해집니다.

♻️ 제안하는 수정
-  const limit = isMobileDevice ? 5 : 9999;
+  const limit = isMobileDevice ? 5 : Infinity;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0df9836 and d484bdb.

📒 Files selected for processing (5)
  • src/app/(main)/teampsylog/_components/KeywordItem.tsx
  • src/components/common/MobileHeader.tsx
  • src/components/modal/ApplyCompleteModal.tsx
  • src/components/profile/ProfileTag.tsx
  • src/components/recruit/ProcessMethod.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/profile/ProfileTag.tsx (1)
src/hooks/useIsMobile.ts (1)
  • useIsMobile (5-19)
src/components/modal/ApplyCompleteModal.tsx (1)
src/constants/ModalList.ts (1)
  • ApplyCompleteModalProps (39-42)
🔇 Additional comments (9)
src/components/common/MobileHeader.tsx (2)

16-16: 헤더 레이어링 개선 확인 완료

relative z-30 추가로 헤더가 다른 UI 요소들과 겹칠 때 적절한 스타킹 순서를 유지합니다. QA 반영 목적에 부합하는 좋은 개선입니다.


20-20: 인터랙티브 피드백 개선

cursor-pointer 추가로 뒤로가기 버튼의 클릭 가능성을 명확하게 표시합니다. 사용자 경험 개선에 도움이 됩니다.

src/components/modal/ApplyCompleteModal.tsx (2)

8-11: React Query 통합 확인

쿼리 클라이언트를 올바르게 import하고 초기화했습니다. 지원 후 프로젝트 상세 정보를 최신 상태로 유지하기 위한 적절한 설정입니다.


22-25: 모달 닫기 로직 통합 잘됨

모든 닫기 경로(ESC 키, 배경 클릭, 버튼 클릭)가 handleClose를 통해 일관되게 쿼리 무효화를 수행합니다. handleClick에서 handleClose를 await한 후 네비게이션하는 순서도 적절합니다.

Also applies to: 28-28, 44-44

src/components/recruit/ProcessMethod.tsx (1)

20-20: 현재 구현은 적절하며 변경이 필요하지 않습니다.

defaultValue="ONLINE"은 유효한 값이고, 현재 구조는 react-hook-form의 합리적인 패턴을 따릅니다. 폼의 useRecruitForm 훅에서 create 모드 시 progressWayundefined로 초기화하므로, Controller 레벨의 defaultValue는 필드 초기값을 올바르게 제공합니다.

  • "ONLINE"DURATION_OPTIONS의 유효한 값입니다 (인덱스 1)
  • 폼 레벨 기본값(undefined)과 충돌하지 않습니다
  • form.reset() 동작도 정상입니다

Likely an incorrect or invalid review comment.

src/components/profile/ProfileTag.tsx (1)

47-51: 타임아웃 정리 로직이 올바르게 구현되었습니다.

컴포넌트 언마운트 시 타임아웃을 정리하여 메모리 누수를 방지하고 있습니다.

src/app/(main)/teampsylog/_components/KeywordItem.tsx (3)

4-11: 인터페이스가 명확하게 정의되었습니다.

KeywordItemProps 인터페이스가 모든 props를 적절한 선택적 마커와 함께 정의하고 있습니다.


97-116: 툴팁 구현이 잘 되어 있습니다.

상대 위치 지정, role="tooltip", aria-label, tabIndex 등 접근성 기능이 적절하게 구현되어 있습니다. 이 방식을 ProfileTag.tsx에도 적용하면 일관성과 안정성이 향상됩니다.


53-80: 이벤트 핸들러가 적절한 조건 검사와 함께 구현되었습니다.

isEditModeisTruncated 조건을 확인하여 불필요한 상태 업데이트를 방지하고, 타임아웃 정리도 올바르게 처리하고 있습니다.

Copy link
Member

@woneeeee woneeeee left a comment

Choose a reason for hiding this comment

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

확인했습니다!! 고생하셨어요 ㅠ 🥹

Comment on lines 33 to 64
const handleMouseEnter = (e: React.MouseEvent<HTMLSpanElement>) => {
if (!isMobile && isTruncated) {
const rect = (e.target as HTMLElement).getBoundingClientRect();
setTooltipPos({ left: rect.left + window.scrollX, top: rect.bottom + window.scrollY });
setShowTooltip(true);
}
};
const handleMouseLeave = () => {
setShowTooltip(false);
setTooltipPos(null);
};

const timeoutRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);

const handleTouch = (e: React.TouchEvent<HTMLSpanElement>) => {
if (isMobile && isTruncated) {
const rect = (e.target as HTMLElement).getBoundingClientRect();
setTooltipPos({ left: rect.left + window.scrollX, top: rect.bottom + window.scrollY });
setShowTooltip(true);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
setShowTooltip(false);
setTooltipPos(null);
}, 1500);
}
};
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

@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

🤖 Fix all issues with AI agents
In @src/app/(main)/teampsylog/_components/KeywordItem.tsx:
- Around line 53-54: The build fails because onFocus/onBlur are being assigned
handleMouseEnter/handleMouseLeave which expect React.MouseEvent; update the
handlers to accept focus events by either extending the useTruncatedTooltip hook
to also return handleFocus and handleBlur (e.g., add focus/blur logic and expose
handleFocus, handleBlur) or create thin adapter handlers in KeywordItem.tsx that
translate React.FocusEvent to the existing mouse-based handlers (call
handleMouseEnter/handleMouseLeave inside new handleFocus/handleBlur wrappers),
then assign onFocus={handleFocus} and onBlur={handleBlur} instead of casting;
reference useTruncatedTooltip, handleMouseEnter, handleMouseLeave, handleFocus
and handleBlur when making the change.
🧹 Nitpick comments (4)
src/hooks/useTruncatedTooltip.ts (2)

21-21: 반환된 ref가 훅 내부에서 사용되지 않음

ref가 반환되지만 훅 내부에서는 사용되지 않습니다. 이벤트 핸들러에서 e.target을 사용하여 위치를 계산하므로 ref가 실제로 필요하지 않습니다. ref를 제거하거나, ref.current를 사용하여 위치 계산을 일관되게 수행하는 것을 고려해 주세요.


24-26: 중복된 타입 체크 제거 가능

maxLength는 이미 TypeScript에서 number 타입으로 지정되어 있고 기본값이 5이므로, Line 25의 typeof maxLength === 'number' 체크는 불필요합니다.

♻️ 제안된 수정
  const getTextLengthWithoutSpaces = (t: string) => t.replace(/\s/g, '').length;
-  const limit = typeof maxLength === 'number' ? maxLength : 5;
+  const limit = maxLength;
  const isTruncated = getTextLengthWithoutSpaces(text) > limit;
src/components/profile/ProfileTag.tsx (1)

12-19: 훅에서 tooltipPos를 사용하지 않음

useTruncatedTooltip 훅에서 tooltipPos를 destructure하지 않고 CSS 기반 절대 위치 지정을 사용하고 있습니다. 이 접근 방식은 유효하지만, 훅의 tooltipPos 계산이 불필요하게 실행됩니다. 다른 컴포넌트도 동일한 패턴을 사용한다면, 훅에서 위치 계산을 선택적으로 만드는 것을 고려해 주세요.

src/app/(main)/teampsylog/_components/KeywordPage.tsx (1)

183-205: 공유 모드용 합성 프로필 객체 추출 권장

공유 모드에서 사용되는 합성 프로필 객체가 인라인으로 정의되어 있어 가독성이 떨어집니다. 별도 함수나 상수로 추출하면 코드가 더 깔끔해집니다.

♻️ 제안된 리팩토링
const createShareProfile = (userId: number | null, name: string | undefined) => ({
  profileId: userId ?? 0,
  userId: userId ?? 0,
  userName: name ?? '',
  profileImageUrl: '',
  profileName: name ?? '',
  position: '',
  workingTime: '',
  links: [],
  contactWay: '',
  headKeywords: [],
  createdAt: '',
  modifiedAt: '',
  uuid: '',
});

// 사용
<LogTitle
  profiles={share ? [createShareProfile(shareUserId, requesterName)] : profiles}
/>
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d484bdb and c3a9dbe.

📒 Files selected for processing (4)
  • src/app/(main)/teampsylog/_components/KeywordItem.tsx
  • src/app/(main)/teampsylog/_components/KeywordPage.tsx
  • src/components/profile/ProfileTag.tsx
  • src/hooks/useTruncatedTooltip.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/(main)/teampsylog/_components/KeywordItem.tsx (1)
src/hooks/useTruncatedTooltip.ts (1)
  • useTruncatedTooltip (14-83)
src/components/profile/ProfileTag.tsx (2)
src/hooks/useIsMobile.ts (1)
  • useIsMobile (5-19)
src/hooks/useTruncatedTooltip.ts (1)
  • useTruncatedTooltip (14-83)
🪛 GitHub Actions: CI/CD Build Check
src/app/(main)/teampsylog/_components/KeywordItem.tsx

[error] 53-53: Conversion of type '(e: React.MouseEvent) => void' to type 'FocusEventHandler' may be a mistake because neither type sufficiently overlaps with the other. The onFocus handler is using a mouse event type. Consider using a proper FocusEventHandler or cast to unknown first.

🔇 Additional comments (8)
src/hooks/useTruncatedTooltip.ts (2)

28-40: 텍스트 truncation 로직이 잘 구현됨

공백을 제외한 문자 수를 기준으로 truncation하는 로직이 명확하게 구현되어 있습니다.


42-71: 이벤트 핸들러 및 클린업 로직 LGTM

타임아웃 클린업, 마우스/터치 이벤트 핸들러가 적절하게 구현되어 있습니다. 모바일에서 1.5초 후 자동 숨김 처리도 좋은 UX입니다.

src/components/profile/ProfileTag.tsx (1)

22-37: 툴팁 렌더링 구조 LGTM

relative wrapper와 absolute 툴팁을 사용한 구조가 깔끔하고, CSS transform으로 중앙 정렬이 잘 적용되어 있습니다.

src/app/(main)/teampsylog/_components/KeywordItem.tsx (2)

4-11: KeywordItemProps 인터페이스 정의 LGTM

Props 인터페이스가 명확하게 정의되어 있고, 선택적 속성들에 적절한 기본값이 설정되어 있습니다.


60-67: 툴팁 접근성 개선 좋음

role="tooltip"aria-label 속성을 사용하여 접근성을 고려한 구현이 좋습니다.

src/app/(main)/teampsylog/_components/KeywordPage.tsx (3)

30-53: Hydration 가드 및 로그인 체크 로직 LGTM

_hasHydrated를 먼저 체크하여 SSR/클라이언트 불일치를 방지하고, 리다이렉트 전 현재 경로를 저장하는 로직이 잘 구현되어 있습니다.


159-172: 다중 로딩 가드 구현 좋음

hydration 대기, 로그인 리다이렉트 대기, 데이터 로딩 각각에 대한 Loading 상태 처리가 적절합니다.


218-218: 이 코드는 의도된 동작입니다. 수정이 필요하지 않습니다.

isShareMode={!share}는 버그가 아니라 의도된 설계입니다. KeywordBar 컴포넌트 내부에서 if (!isShareMode) return null;로 처리되므로:

  • share=true (공유 모드): isShareMode=false → KeywordBar 렌더링 안 됨 ✓
  • share=false (편집 모드): isShareMode=true → KeywordBar 렌더링 됨 ✓

이는 LogNote에 isShareMode={share}를 전달하는 것과 일관성 있는 패턴입니다. KeywordBar는 편집 제어 바이므로 공유 모드에서는 숨겨져야 하고, 이 로직이 그것을 올바르게 구현합니다.

Comment on lines 53 to 54
onFocus={handleMouseEnter as React.FocusEventHandler<HTMLSpanElement>}
onBlur={handleMouseLeave as React.FocusEventHandler<HTMLSpanElement>}
Copy link

@coderabbitai coderabbitai bot Jan 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

타입 불일치로 인한 빌드 오류

파이프라인 오류에서 지적된 것처럼, handleMouseEnterReact.MouseEvent를 기대하지만 onFocusReact.FocusEvent를 제공합니다. 이벤트 타입이 호환되지 않아 빌드가 실패합니다.

🐛 제안된 수정

useTruncatedTooltip 훅에 focus/blur 핸들러를 추가하거나, 컴포넌트에서 별도 핸들러를 생성해야 합니다:

-        onFocus={handleMouseEnter as React.FocusEventHandler<HTMLSpanElement>}
-        onBlur={handleMouseLeave as React.FocusEventHandler<HTMLSpanElement>}
+        onFocus={() => {
+          if (!isMobileDevice && isTruncated) {
+            // Focus 시 tooltip 표시를 위한 로직 필요
+          }
+        }}
+        onBlur={() => {
+          // handleMouseLeave는 파라미터를 사용하지 않으므로 직접 호출 가능
+          handleMouseLeave();
+        }}

또는 훅에서 handleFocushandleBlur를 별도로 제공하는 것을 권장합니다.

🧰 Tools
🪛 GitHub Actions: CI/CD Build Check

[error] 53-53: Conversion of type '(e: React.MouseEvent) => void' to type 'FocusEventHandler' may be a mistake because neither type sufficiently overlaps with the other. The onFocus handler is using a mouse event type. Consider using a proper FocusEventHandler or cast to unknown first.

🤖 Prompt for AI Agents
In @src/app/(main)/teampsylog/_components/KeywordItem.tsx around lines 53 - 54,
The build fails because onFocus/onBlur are being assigned
handleMouseEnter/handleMouseLeave which expect React.MouseEvent; update the
handlers to accept focus events by either extending the useTruncatedTooltip hook
to also return handleFocus and handleBlur (e.g., add focus/blur logic and expose
handleFocus, handleBlur) or create thin adapter handlers in KeywordItem.tsx that
translate React.FocusEvent to the existing mouse-based handlers (call
handleMouseEnter/handleMouseLeave inside new handleFocus/handleBlur wrappers),
then assign onFocus={handleFocus} and onBlur={handleBlur} instead of casting;
reference useTruncatedTooltip, handleMouseEnter, handleMouseLeave, handleFocus
and handleBlur when making the change.

✅ Addressed in commit 3441a5f

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(main)/teampsylog/_components/KeywordItem.tsx (1)

39-41: 모바일 터치 이벤트가 onClick까지 전파되어 툴팁 표시 의도가 훼손될 수 있습니다.

onTouchStart에서 handleTouch가 실행되어 툴팁을 표시하지만(1.5초 타임아웃), 터치 이벤트가 상위 div로 전파되어 onClick도 즉시 실행됩니다. onClick이 라우팅이나 모달 오픈 등의 작업을 수행하면 컴포넌트가 언마운트되어 툴팁이 표시되지 않습니다. 모바일에서 truncation이 발생한 경우, 터치 시 툴팁만 표시되도록 이벤트 전파를 차단해야 합니다.

수정 예시: 모바일 + truncated 상태에서 터치 이벤트 전파 차단
-        onTouchStart={handleTouch as React.TouchEventHandler<HTMLSpanElement>}
+        onTouchStart={(e) => {
+          handleTouch(e);
+          if (isMobileDevice && isTruncated) e.stopPropagation();
+        }}
🧹 Nitpick comments (4)
src/app/(main)/teampsylog/_components/KeywordItem.tsx (4)

4-11: KeywordItemProps를 외부에서 쓰는 타입이면 export로 공개하세요.

지금은 interface KeywordItemProps가 파일 내부 스코프라, 다른 컴포넌트에서 재사용하려면 export가 필요합니다.

Proposed diff
-interface KeywordItemProps {
+export interface KeywordItemProps {
   keyword: string;
   isEditMode?: boolean;
   isSelected?: boolean;
   isPlaceholder?: boolean;
   onClick?: () => void;
   isMobileDevice?: boolean;
 }

21-23: 데스크탑 9999 매직넘버는 의도를 코드로 드러내는 값으로 바꾸는 게 좋아요.

사실상 “무제한”이면 Number.POSITIVE_INFINITY 같은 값이 더 명확합니다(또는 훅에서 “비활성화” 옵션 지원).

Proposed diff
-  // limit: 모바일 5, 데스크탑 9999 (사실상 무제한)
-  const maxLength = isMobileDevice ? 5 : 9999;
+  // limit: 모바일 5, 데스크탑 무제한
+  const maxLength = isMobileDevice ? 5 : Number.POSITIVE_INFINITY;

24-37: 이벤트/Ref 캐스팅이 많아 타입 안정성이 떨어집니다(훅을 제네릭으로 만드는 쪽 추천).

spanRef as ..., handleTouch as ..., handleMouseEnter(e as unknown as ...) 형태는 유지보수 때 깨지기 쉽습니다. useTruncatedTooltip<T extends HTMLElement>()ref/핸들러 타입을 HTMLSpanElement까지 맞추는 리팩터링을 추천합니다.

Also applies to: 53-59


60-72: 툴팁 접근성: aria-describedby로 tooltip과 연결하는 개선 여지 있습니다.

현재는 aria-label만 있어 스크린리더가 “툴팁 콘텐츠”를 관계로 인지하기 어렵습니다(특히 role="tooltip"을 쓸 때). id 부여 후 aria-describedby 연결을 고려해 주세요.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3a9dbe and 3441a5f.

📒 Files selected for processing (1)
  • src/app/(main)/teampsylog/_components/KeywordItem.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/(main)/teampsylog/_components/KeywordItem.tsx (1)
src/hooks/useTruncatedTooltip.ts (1)
  • useTruncatedTooltip (14-83)

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: 3

🤖 Fix all issues with AI agents
In @src/app/(main)/teampsylog/_components/KeywordItem.tsx:
- Around line 34-41: handleFocus currently forces-casts undefined to
React.MouseEvent and calls handleMouseEnter, which is unsafe; update the logic
to avoid passing a fake event: modify the useTruncatedTooltip hook to expose
explicit showTooltipManually/hideTooltipManually methods (or make
handleMouseEnter accept an optional event), then change handleFocus to call
showTooltipManually() and handleBlur to call hideTooltipManually() (or use the
optional-event-safe handleMouseEnter/handleMouseLeave) so no undefined event is
cast and tooltip display is handled safely.
- Around line 24-32: The hook useTruncatedTooltip is currently typed with a
generic HTMLElement causing three unsafe casts where it's used: spanRef,
handleMouseEnter/handleMouseLeave, and handleTouch; update useTruncatedTooltip's
generic to HTMLSpanElement and adjust its handler types to return
React.MouseEventHandler<HTMLSpanElement> for mouse handlers and
React.TouchEventHandler<HTMLSpanElement> for touch handler so callers (e.g.,
spanRef, handleMouseEnter, handleMouseLeave, handleTouch) receive correctly
typed values and the explicit casts can be removed.
🧹 Nitpick comments (4)
src/app/(main)/teampsylog/_components/KeywordItem.tsx (4)

21-22: 주석 표현 개선 제안

주석에 "사실상 무제한"이라고 표기했지만, 9999는 여전히 하드 리미트입니다. 실무에서 키워드가 이 길이를 초과할 가능성은 낮지만, 더 정확한 표현을 위해 "충분히 큰 값" 정도로 수정하는 것을 고려해 보세요.

💡 주석 개선 제안
-  // limit: 모바일 5, 데스크탑 9999 (사실상 무제한)
+  // limit: 모바일 5, 데스크탑 9999 (충분히 큰 값으로 설정)

59-62: 불필요한 타입 캐스팅 제거 검토

이벤트 핸들러에 타입 캐스팅을 하고 있는데, useTruncatedTooltip 훅이 올바른 타입을 반환한다면 이러한 캐스팅은 불필요합니다. 훅의 타입 정의를 개선하여 타입 안정성을 확보하는 것을 권장합니다.

♻️ 타입 정의 개선 제안

useTruncatedTooltip 훅에서 다음과 같이 명시적 타입을 반환하도록 수정:

// useTruncatedTooltip.ts
export const useTruncatedTooltip = ({ text, maxLength, isMobile }: Props) => {
  // ... 구현
  
  return {
    ref,
    showTooltip,
    displayText,
    isTruncated,
    handleMouseEnter: handleMouseEnter as React.MouseEventHandler<HTMLElement>,
    handleMouseLeave: handleMouseLeave as React.MouseEventHandler<HTMLElement>,
    handleTouch: handleTouch as React.TouchEventHandler<HTMLElement>,
  };
};

그러면 이 컴포넌트에서 캐스팅 제거 가능:

      <span
        ref={spanRef as React.RefObject<HTMLSpanElement>}
-       onTouchStart={handleTouch as React.TouchEventHandler<HTMLSpanElement>}
-       onMouseEnter={handleMouseEnter as React.MouseEventHandler<HTMLSpanElement>}
-       onMouseLeave={handleMouseLeave as React.MouseEventHandler<HTMLSpanElement>}
+       onTouchStart={handleTouch}
+       onMouseEnter={handleMouseEnter}
+       onMouseLeave={handleMouseLeave}
        onFocus={handleFocus}
        onBlur={handleBlur}

58-79: 중첩된 인터랙티브 요소로 인한 접근성 문제 검토

45번 라인의 외부 divonClick을 가지고 있고, 58-78번 라인의 내부 spantabIndex={0}과 포커스 핸들러를 가지고 있습니다. 이러한 중첩된 인터랙티브 요소는 다음과 같은 문제를 일으킬 수 있습니다:

  1. 키보드 탐색 혼란: 사용자가 Tab 키로 이동할 때 span에 포커스되지만, 실제 클릭 동작은 외부 div가 처리합니다.
  2. 스크린 리더 혼란: 보조 기술 사용자가 이 요소의 목적을 정확히 이해하기 어려울 수 있습니다.
  3. 의도치 않은 동작: truncated되지 않은 경우에도 외부 div는 클릭 가능하지만, span은 포커스 불가능합니다.

권장 사항:

  • isEditMode가 false이고 truncated된 경우에만 툴팁 기능을 제공하고, 클릭 동작은 외부 div에서만 처리하도록 구조를 단순화하는 것을 고려해 보세요.
  • 또는 truncated 상태와 관계없이 일관된 포커스 동작을 제공하세요.
♻️ 구조 개선 제안

옵션 1: 툴팁 전용 span과 클릭 영역 분리

  return (
    <div
      onClick={onClick}
+     tabIndex={onClick && !isEditMode ? 0 : undefined}
+     onKeyDown={(e) => {
+       if (e.key === 'Enter' || e.key === ' ') {
+         onClick?.();
+       }
+     }}
      className={`desktop:body-5 body-9 desktop:px-5 desktop:py-3 box-border rounded-lg px-3 py-2 transition-all ${
        // ... 기존 className
      } `}
    >
      <span
        ref={spanRef as React.RefObject<HTMLSpanElement>}
        onTouchStart={handleTouch as React.TouchEventHandler<HTMLSpanElement>}
        onMouseEnter={handleMouseEnter as React.MouseEventHandler<HTMLSpanElement>}
        onMouseLeave={handleMouseLeave as React.MouseEventHandler<HTMLSpanElement>}
-       onFocus={handleFocus}
-       onBlur={handleBlur}
-       tabIndex={!isEditMode && isTruncated ? 0 : undefined}
-       className="relative cursor-pointer"
+       className="relative"
        aria-label={isTruncated ? keyword : undefined}
      >
        #{displayText}
        {showTooltip && (
          <span
            role="tooltip"
            className="absolute top-full left-1/2 z-20 mt-1 -translate-x-1/2 rounded bg-black/70 px-2 py-1 text-xs whitespace-nowrap text-white shadow-lg"
          >
            #{keyword}
          </span>
        )}
      </span>
    </div>
  );

이 방식에서는:

  • 외부 div가 키보드 포커스와 클릭을 모두 처리
  • 내부 span은 순수하게 툴팁 표시만 담당
  • 일관된 인터랙션 모델 제공

66-66: 중복된 cursor-pointer 스타일 제거 검토

span에 cursor-pointer 클래스가 적용되어 있지만, 실제 클릭 동작은 외부 div(45번 라인)가 처리합니다. 이는 사용자에게 혼란을 줄 수 있으므로, span의 cursor-pointer를 제거하거나 외부 div의 조건부 스타일에 통합하는 것을 고려해 보세요.

♻️ 제거 제안
      <span
        ref={spanRef as React.RefObject<HTMLSpanElement>}
        onTouchStart={handleTouch as React.TouchEventHandler<HTMLSpanElement>}
        onMouseEnter={handleMouseEnter as React.MouseEventHandler<HTMLSpanElement>}
        onMouseLeave={handleMouseLeave as React.MouseEventHandler<HTMLSpanElement>}
        onFocus={handleFocus}
        onBlur={handleBlur}
        tabIndex={!isEditMode && isTruncated ? 0 : undefined}
-       className="relative cursor-pointer"
+       className="relative"
        aria-label={isTruncated ? keyword : undefined}
      >
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3441a5f and 2e769a7.

📒 Files selected for processing (1)
  • src/app/(main)/teampsylog/_components/KeywordItem.tsx

@sunhwaaRj sunhwaaRj merged commit 4330be7 into develop Jan 11, 2026
2 checks passed
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.

[FEATURE] 릴리즈 이전 QA 사항 반영

2 participants