Skip to content

QA: 나머지 qa 반영#188

Merged
seueooo merged 10 commits into
developfrom
feat/qa/#186
Aug 21, 2025
Merged

QA: 나머지 qa 반영#188
seueooo merged 10 commits into
developfrom
feat/qa/#186

Conversation

@seueooo
Copy link
Copy Markdown
Contributor

@seueooo seueooo commented Aug 21, 2025

📌 Summary

📚 Tasks

  • 우선순위 높은 qa 모두 반영했습니다.
  • 편지담기에서, 기존 텍스트에서 스피너 라이브러리에서 가져와서 로딩상태 표시되도록 수정했습니다.

Summary by CodeRabbit

  • New Features

    • 캡슐 생성 완료 전 2초 로딩 화면 추가
    • 다음 단계 버튼이 제목 입력이 비어있으면 비활성화됨
  • Bug Fixes / Behavior

    • 캡슐 상세 모달: 오버레이 클릭으로 더 이상 닫히지 않음
  • Style

    • 카드 그리드 기본 간격 축소, 1024px 이상에서 간격 복원
    • 드롭다운 폭(18rem), 항목 높이, 모서리 둥글림 및 열림/닫힘 애니메이션 추가
    • 쓰기 모달 타이틀 중앙 정렬 및 팝업 타이틀/콘텐츠 여백 조정
    • 편지 제출 버튼에 로더 표시(업로드/제출 중)
    • Chip 컴포넌트 크기 옵션 제거로 스타일 일관화

@seueooo seueooo self-assigned this Aug 21, 2025
@seueooo seueooo requested a review from seung365 as a code owner August 21, 2025 11:48
@seueooo seueooo linked an issue Aug 21, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 21, 2025

Walkthrough

여러 컴포넌트의 레이아웃/간격을 조정하고, 드롭다운에 애니메이션 기반 열림/닫힘 제어를 추가했습니다. 캡슐 생성 성공 후 2초 로딩 화면을 도입했고, 특정 모달의 오버레이 클릭 닫기를 제거했습니다. 타입 및 Chip API 일부가 변경되었습니다.

Changes

Cohort / File(s) Change Summary
카드 컨테이너 간격 조정
app/(main)/explore/.../card-container.css.ts, app/(main)/my-capsule/.../card-container.css.ts, app/(sub)/search/.../card-container.css.ts
기본 gap 1.2rem→0.8rem, rowGap 3.2rem→2.4rem. 1024px 이상에서 이전 간격(1.2rem/3.2rem) 명시. 컬럼 수 변경 없음.
모달 오버레이 클릭 닫기 제거
app/(sub)/capsule-detail/_components/modal/index.tsx
overlay onClick 제거 및 handleOverlayClick 삭제; 키보드 닫기 로직은 유지.
쓰기 모달: 로더 및 필드명 변경
app/(sub)/capsule-detail/_components/write-modal/index.tsx, app/(sub)/capsule-detail/_components/write-modal/write-modal.css.ts, shared/types/api/letter.ts
제출/업로드 중 버튼에 PulseLoader 표시로 교체. WriteLetterReq.objectKeys → objectKey로 필드명 변경 반영. 제목 스타일에 flex 중앙 정렬 추가.
캡슐 생성: 2초 로딩 흐름 추가
app/(sub)/create-capsule/page.tsx
mutation 성공 직후 완료로 이동하던 흐름을 제거하고, isPending 기반 showLoading을 사용해 성공 후 2초 동안 로딩 화면을 표시한 뒤 step을 "complete"로 전환.
인트로 스텝 유효성 변경
app/(sub)/create-capsule/_components/steps/intro-step/index.tsx
getValues 사용 제거, Next 버튼을 제목 트림 검사로 disabled 처리(인라인 alert 제거).
캡슐 뮤테이션 캐시 무효화 단순화
shared/api/mutations/capsule.ts
여러 대상 무효화(detail(id), lists, my 등) 대신 onSuccess에서 capsuleQueryKeys.all()로 통합 invalidation. like/leave mutation 반환값 및 핸들러 시그니처 정리.
드롭다운 애니메이션 및 상태관리 추가
shared/ui/dropdown/dropdown.css.ts, shared/ui/dropdown/index.tsx
slideIn/slideOut 키프레임 추가. dropdownContent에 열림/닫힘 애니메이션 적용, closing용 스타일 export 추가. isAnimating/isClosing 상태와 애니메이션 수명주기(useEffect) 도입, 외부 클릭 처리 조건화, width 19.2rem→18rem, item 높이 추가.
팝업 취소 UI 스타일 추가
shared/ui/popup/popup-cancel-creation/index.tsx, .../popup-cancel-creation.css.ts
Popup.Content에 className 적용. content 스타일(F16) 신규 export. title에 marginBottom 추가.
확인 팝업 타이틀 간격 조정
shared/ui/popup/popup-confirm-letter/popup-confirm-letter.css.ts
title의 gap 제거(기타 속성 유지).
Chip 컴포넌트에서 size 제거 및 스토리 정리
shared/ui/chip/index.tsx, shared/ui/chip/chip.stories.tsx
ChipProps에서 size 제거, size 기반 스타일/사용 제거. Storybook에서 size argType 및 관련 사례 제거.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant P as CreateCapsule Page
  participant M as useCreateCapsule (mutation)
  participant L as CreateCapsuleLoading

  U->>P: 폼 제출
  P->>M: mutate()
  M-->>P: isPending=true
  P->>P: showLoading=true (로딩 화면 렌더)
  alt mutation 성공
    M-->>P: isPending=false
    P->>P: 2초 타이머 시작
    P-->>P: 2초 후 showLoading=false
    P->>P: step="complete"
  end
Loading
sequenceDiagram
  autonumber
  actor U as 사용자
  participant D as Dropdown
  participant C as DropdownContent

  U->>D: 토글 클릭
  alt 닫히는 경우 (열려있음)
    D->>D: isClosing=true, isAnimating=true
    D->>C: slideOut (closing 클래스 적용)
    D->>D: 150ms 후 open=false, isClosing=false, isAnimating=false
  else 열리는 경우 (닫혀있음)
    D->>D: open=true, isAnimating=true, isClosing=false
    D->>C: slideIn
    D->>D: 애니메이션 후 isAnimating=false
  end

  rect rgba(0,0,0,0.03)
  note over D,C: isAnimating/isClosing 동안 상호작용 제한, 외부 클릭은 open일 때만 처리
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • seung365

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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/qa/#186

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 21, 2025

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

Project Deployment Preview Comments Updated (UTC)
time-capsule Ready Ready Preview Comment Aug 21, 2025 0:02am

@github-actions
Copy link
Copy Markdown

This pull request (commit d3a27fd) has been deployed to Vercel ▲ - View GitHub Actions Workflow Logs

Name Link
🌐 Unique https://time-capsule-qxtiwcfzq-hs-projects-b4a69d5f.vercel.app
🔍 Inspect https://vercel.com/hs-projects-b4a69d5f/time-capsule/DfakRUrXdtMfMDYosSGXCEJJ7zZR

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 21, 2025

🚀 Storybook 배포

📖 Storybook: https://683d91ab23651aa0b399e435-wylaapvfjf.chromatic.com/
🔗 Chromatic Build: https://www.chromatic.com/build?appId=683d91ab23651aa0b399e435&number=214
✅ Status: success

Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (3)
shared/ui/popup/popup-cancel-creation/index.tsx (1)

27-29: 버튼 레이블 오탈자 추정: '남기' → '계속 만들기' 또는 '남기기'

사용자 노출 텍스트로 보아 '남기'는 비문에 가깝습니다. 대화형 맥락(“만들기를 그만할까요?”)상 반대 액션은 “계속 만들기”가 자연스러워 보입니다. 디자인 가이드에 따라 ‘남기기’가 의도였다면 그쪽으로 정정해 주세요.

다음과 같이 수정 제안드립니다:

-          남기
+          계속 만들기

원문 텍스트 표준을 유지하려면 i18n 키로 분리해 관리하는 것도 추천드립니다.

shared/ui/dropdown/index.tsx (2)

118-127: Trigger 버튼 기본 type 누락 → 폼 내 예기치 않은 submit 유발 가능

button의 기본 type은 "submit"입니다. 폼 내부에서 드롭다운 트리거 클릭 시 폼 제출이 발생할 수 있어요. 또한 ARIA 속성으로 상태를 노출하면 접근성이 좋아집니다.

-    <button
+    <button
+      type="button"
       onClick={(e) => {
         e.stopPropagation();
         handleToggleOpen();
         onClick?.();
       }}
       className={cn(styles.triggerBtnStyle, className)}
+      aria-haspopup="menu"
+      aria-expanded={open}
     >

172-188: Item 버튼 기본 type 누락 → 폼 내 submit 방지 필요

DropdownItem도 동일하게 기본 type 지정이 필요합니다. props로 type을 넘기면 override 가능하도록 스프레드 뒤쪽이 아닌 앞쪽에 "button"을 두는 현재 제안이 안전합니다.

       <button
-        {...props}
+        type="button"
+        {...props}
         onClick={(e) => {
           e.stopPropagation();
           handleToggleClose();
           onClick?.();
         }}
         className={cn(styles.dropdownItem, className)}
         role="menuitem"
         aria-label={label}
       >
🧹 Nitpick comments (26)
app/(sub)/create-capsule/_components/steps/intro-step/index.tsx (2)

19-31: 공백 전용 입력 UX 정합성: 버튼 비활성화와 글자수 카운트를 동일한 기준(trim)으로 통일

현재 버튼 disabledtrim() 기준이지만 글자수 카운트는 원본 길이를 사용하고 있어, 공백만 입력 시 “카운트는 증가하지만 버튼은 여전히 비활성화”인 불일치가 생깁니다. 동일한 트림 기준으로 맞추면 혼란을 줄일 수 있습니다. 렌더마다 trim() 호출을 줄이도록 파생값으로 계산해 재사용하면 가독성도 좋아집니다.

   const { field: titleField } = useController({
     name: "title",
     control,
   });
 
   const { field: subtitleField } = useController({
     name: "subtitle",
     control,
   });
 
+  // 공백 제거 기준을 한 곳에서 재사용
+  const trimmedTitle = (typeof titleField.value === "string" ? titleField.value : "").trim();
+
   const handleClickNext = () => {
     handleNextStep("date");
   };
@@
-          <span className={styles.charCountTitle}>{titleField.value?.length || 0}/15</span>
+          <span className={styles.charCountTitle}>{trimmedTitle.length}/15</span>
@@
-          <Button variant="primary" text="다음" onClick={handleClickNext} disabled={!titleField.value?.trim()} />
+          <Button variant="primary" text="다음" onClick={handleClickNext} disabled={trimmedTitle.length === 0} />

참고: 위의 첫 번째 코멘트처럼 defaultValue: ""를 주면 trim() 호출 시 런타임 타입 오류 가능성도 사실상 제거됩니다.

Also applies to: 63-64, 78-79


17-27: useController의 defaultValue 생략 가능 & 제목 검증 추가 권장

app/(sub)/create-capsule/page.tsx에서 이미 useForm({ defaultValues: { title: "", subtitle: "", … } })로 기본값을 지정하고 있어, useControllerdefaultValue를 별도 설정할 필요가 없습니다.
다만, handleNextStep 등 프로그램적으로 이동할 때도 빈 문자열이 넘어가지 않도록 titleField에 검증 로직을 추가하는 것을 권장드립니다. (react-hook-form.com)

• 파일: app/(sub)/create-capsule/_components/steps/intro-step/index.tsx
• 위치: useController 호출 부분

옵셔널 제안:

 const { field: titleField } = useController({
   name: "title",
   control,
-  // defaultValue: ""  // useForm defaultValues로 이미 설정됨
+  rules: {
+    validate: (v) =>
+      (typeof v === "string" && v.trim().length > 0) ||
+      "제목은 공백일 수 없어요.",
+  },
 });
app/(main)/my-capsule/_components/card-container/card-container.css.ts (1)

6-7: 간격 값 하드코딩 대신 토큰/공통 상수로 치환 권장 (세 파일 모두 동일 패턴).

세로/가로 간격을 명시적으로 분리해 둔 점은 좋습니다. 다만 "0.8rem / 2.4rem" 상수를 각 파일에서 반복 정의하고 있어 유지보수 시 드리프트 가능성이 있습니다. 공통 토큰(또는 상수)로 추출해 재사용해주세요.

적용 예시(현재 파일 내 상수 도입):

// 파일 상단에 추가
const COLUMN_GAP_SM = "0.8rem";
const ROW_GAP_SM = "2.4rem";
const COLUMN_GAP_LG = "1.2rem";
const ROW_GAP_LG = "3.2rem";

해당 라인 변경 diff:

-  gap: "0.8rem",
-  rowGap: "2.4rem",
+  gap: COLUMN_GAP_SM,
+  rowGap: ROW_GAP_SM,

또는 디자인 토큰이 있다면(예: theme.space) 그 값으로 교체하는 것을 추천합니다.

app/(main)/explore/_components/card-container/card-container.css.ts (1)

5-6: 중복 숫자 제거로 유지보수성 향상 제안.

my-capsule, explore, search 세 곳 모두 동일한 간격 스케일(0.8/2.4/1.2/3.2rem)을 사용합니다. 공통 스타일(예: baseCardGrid)로 추출해 style([base, overrides]) 형태로 합성하면 변경 범위를 한 곳으로 한정할 수 있습니다.

라인 치환 예시:

-  gap: "0.8rem",
-  rowGap: "2.4rem",
+  gap: COLUMN_GAP_SM,
+  rowGap: ROW_GAP_SM,

공통화 아이디어(별도 파일 예시):

// styles/cardGrid.css.ts (예시)
export const baseCardGrid = style({
  display: "grid",
  gap: COLUMN_GAP_SM,
  rowGap: ROW_GAP_SM,
  "@media": {
    "(min-width: 1024px)": { gap: COLUMN_GAP_LG, rowGap: ROW_GAP_LG },
  },
});
app/(sub)/search/_components/card-container/card-container.css.ts (2)

5-6: 하드코딩된 간격 값 공통화/토큰화 제안.

앞선 두 파일과 동일한 코멘트입니다. 간격 스케일을 상수/토큰으로 추출하여 세 파일에서 일관되게 참조하면 향후 QA 조정 시 리스크를 크게 줄일 수 있습니다.

라인 치환 예시:

-  gap: "0.8rem",
-  rowGap: "2.4rem",
+  gap: COLUMN_GAP_SM,
+  rowGap: ROW_GAP_SM,

19-20: 사소한 일관성: 속성 순서 통일 제안.

이 파일은 1024px 블록에서 gap/rowGap → gridTemplateColumns 순서인데, 다른 두 파일은 gridTemplateColumns → gap/rowGap 순서입니다. 기능 차이는 없지만 팀 코딩 컨벤션 측면에서 정렬을 맞추면 읽기 쉬워집니다.

shared/ui/popup/popup-cancel-creation/index.tsx (1)

21-24: 내비게이션/닫기 순서 안정화 또는 replace 고려

router.pushclose()를 호출하면 라우팅 타이밍에 따라 언마운트된 컴포넌트로 setState가 전파될 여지가 있습니다. 우선순위를 바꿔 먼저 close()로 상태를 정리하고 라우팅하는 편이 안전합니다. 돌아가기 허용이 불필요하다면 replace도 고려해 주세요.

다음과 같이 순서만 교체:

-            router.push(PATH.EXPLORE);
-            close();
+            close();
+            router.push(PATH.EXPLORE);

또는 히스토리 정리를 원하시면:

-            router.push(PATH.EXPLORE);
+            router.replace(PATH.EXPLORE);
shared/ui/popup/popup-cancel-creation/popup-cancel-creation.css.ts (2)

7-8: 여백 하드코딩 대신 토큰 사용 검토

"0.6rem"은 매직 넘버에 가깝습니다. 디자인 시스템에 spacing 토큰이 있다면 해당 토큰 사용을 권장드립니다(예: themeVars.space.s6 등). 토큰이 없다면 공용 spacing 유틸 도입을 고려해 주세요.


14-16: 공통 팝업 콘텐츠 타이포 사이즈 variant 관리 검토 권장

스크립트 실행 결과, 팝업 내 폰트 크기 사용 현황은 다음과 같습니다.

  • shared/ui/popup/popup.css.ts: 기본 content – F14
  • shared/ui/popup/popup-intro/popup-intro.css.ts: F16(34행), F14(61행)
  • shared/ui/popup/popup-report/popup-report.css.ts: F16(30행)
  • shared/ui/popup/popup-exit-lettie/popup-exit-lettie.css.ts: F16(27행·34행·46행)
  • shared/ui/popup/popup-cancel-creation/popup-cancel-creation.css.ts: F16(15행)
  • shared/ui/popup/popup-confirm-letter/popup-confirm-letter.css.ts: F16(17행)

여러 팝업에서 F16 override가 반복 적용되고 있어, 공용 popup.css.ts에 styleVariants(sm: F14 / md: F16) 형태로 관리하도록 전환하면 일관성과 재사용성이 높아집니다.

현 PR에서는 기존 구현 유지 OK, 추후 다음과 같은 방식으로 리팩토링을 검토해 주세요:

// shared/ui/popup/popup.css.ts
import { style, styleVariants } from "@vanilla-extract/css";
import { themeVars } from "@/shared/styles/base/theme.css";

export const contentBase = style({
  color: themeVars.color.white[40],
  marginBottom: "2.8rem",
  display: "flex",
  flexDirection: "column",
  gap: "0.4rem",
});

export const contentTypography = styleVariants({
  sm: themeVars.text.F14,
  md: themeVars.text.F16,
});

// 사용 예:
// className={`${popupStyles.contentBase} ${popupStyles.contentTypography.md}`}
shared/ui/chip/index.tsx (1)

3-3: 불필요한 import 공백 정리 및 chipSize 토큰 사용 현황

  • 파일: shared/ui/chip/index.tsx
    • 변경 전:
    import { chipBase,  chipVariant } from "./chip.css";
    • 변경 후:
    import { chipBase, chipVariant } from "./chip.css";
  • ./chip.css 경로는 vanilla-extract 설정 및 TypeScript 모듈 해석 옵션에서 일반적으로 문제없으나, 로컬에서 한번 빌드 확인 부탁드립니다.
  • chipSize 토큰(shared/ui/chip/chip.css.ts 라인 27~)은 정의되어 있으나,
    shared/ui/chip/index.tsx에 import 및 적용 로직이 없으며
    <Chip size=…> prop 사용 흔적도 없습니다.
    따라서:
    – size 옵션을 도입할 예정이라면
    1. ChipPropssize?: keyof typeof chipSize 추가
    2. classNamechipSize[size]를 조합
      – 현재 필요 없는 기능이라면 정의된 chipSize 스타일을 제거하는 것을 권장드립니다.
shared/ui/chip/chip.stories.tsx (3)

97-107: AllVariants 스토리: 중복 제거 및 사이즈 문구 제거

size 지원이 사라졌으므로 “Small/Default” 문구는 혼선을 줍니다. 또한 동일 variant를 두 번 렌더링하는 중복을 줄이면 유지보수성이 좋아집니다.

아래처럼 매핑 렌더링으로 간결화하고 라벨을 정리하는 것을 제안드립니다.

-      <Chip variant="purple">
-        Small Purple
-      </Chip>
-      <Chip variant="purple">
-        Default Purple
-      </Chip>
-      <Chip variant="gray">
-        Small Gray
-      </Chip>
-      <Chip variant="gray">
-        Default Gray
-      </Chip>
+      {(["purple", "gray"] as const).map((v) => (
+        <Chip key={v} variant={v}>
+          {v === "purple" ? "Purple" : "Gray"}
+        </Chip>
+      ))}

13-15: 문서 설명에서 미지원 속성(size) 언급 제거

스토리북 문서 설명에 여전히 Small, Default 사이즈 지원 문구가 남아 있어 사용자 혼선을 유발합니다. 현재 컴포넌트 스펙에 맞게 문구를 정리해 주세요.

-          "텍스트나 태그를 표시하는데 사용되는 작은 UI 요소입니다. Purple과 Gray 두 가지 variant와 Small, Default 두 가지 size를 지원합니다.\n\n",
+          "텍스트나 태그를 표시하는 작은 UI 요소입니다. 현재 Purple과 Gray 두 가지 variant를 지원합니다.\n\n",

80-92: Size 제거와 불일치하는 스토리 이름 정리

Small, SmallGray 스토리는 더 이상 의미가 맞지 않습니다. 이름 및 라벨을 현재 스펙에 맞게 정리하거나 통합해 주세요.

-export const Small: Story = {
+export const PurpleExample: Story = {
   args: {
-    children: "Small Chip",
+    children: "Purple Chip",
     variant: "purple",
   },
 };

-export const SmallGray: Story = {
+export const GrayExample: Story = {
   args: {
-    children: "Small Gray",
+    children: "Gray Chip",
     variant: "gray",
   },
 };
shared/ui/dropdown/dropdown.css.ts (3)

5-15: 애니메이션 키프레임의 스케일링 강도를 완화하거나 유지보수성을 높이는 방향 제안

현재 scale(0.5) → scale(1) 전환은 시각적 임팩트가 큰 대신, 텍스트 렌더링 블러·가독성 저하가 있을 수 있습니다. 선호도와 디자인 가이드에 따라 스케일을 완화하거나(예: 0.95~0.98), translate+opacity만 사용하는 것도 고려해주세요.

스케일을 완화하는 예시:

 const slideIn = keyframes({
   from: {
     opacity: 0,
-    transform: "translateY(-10px) scale(0.5)",
+    transform: "translateY(-10px) scale(0.98)",
   },
   to: {
     opacity: 1,
     transform: "translateY(0) scale(1)",
   },
 });

 const slideOut = keyframes({
   from: {
     opacity: 1,
     transform: "translateY(0) scale(1)",
   },
   to: {
     opacity: 0,
-    transform: "translateY(-10px) scale(0.5)",
+    transform: "translateY(-10px) scale(0.98)",
   },
 });

Also applies to: 16-25


38-51: 애니메이션 성능·접근성 보완: will-change, reduced-motion 대응 추가 제안

열림 애니메이션 자체는 적절합니다. 다만 성능(합성 레이어)과 모션 민감 사용자 배려를 위해 will-change와 prefers-reduced-motion 대응을 권장합니다.

 export const dropdownContent = style({
   position: "absolute",
   right: 0,
   top: "100%",
   display: "flex",
   width: "18rem",
   flexDirection: "column",
   backgroundColor: themeVars.color.black[70],
   padding: "0.6rem",
   borderRadius: "15px",
   boxShadow: "0px 4px 16px 0px rgba(0, 0, 0, 0.10)",
   animation: `${slideIn} 0.2s ease-out`,
   transformOrigin: "top right",
+  willChange: "transform, opacity",
+  "@media": {
+    "(prefers-reduced-motion: reduce)": {
+      animation: "none",
+    },
+  },
 });

참고: 닫힘 시 slideOut이 slideIn을 덮어쓰는 것은 동일 특이성일 때 CSS 선언 순서에 좌우됩니다(현재 파일 순서상 OK). 단, 바닐라-익스트랙트 빌드 결과에서도 이 순서가 유지되는지 브라우저별로 한 번만 확인 부탁드립니다.


54-56: 닫힘 중 포인터 차단 및 모션 감소 대응 추가 제안

닫힘 애니메이션 동안 사용자 상호작용을 차단하면 안전합니다. 또한 reduced-motion에서도 애니메이션을 끄는 것을 권장합니다.

 export const dropdownContentClosing = style({
   animation: `${slideOut} 0.15s ease-in`,
+  pointerEvents: "none",
+  "@media": {
+    "(prefers-reduced-motion: reduce)": {
+      animation: "none",
+    },
+  },
 });
shared/ui/dropdown/index.tsx (1)

142-151: 콘텐츠 컨테이너에 role 및 가시성 상태 노출 권장

ul 아래 button에 role="menuitem"을 사용 중이므로 컨테이너는 role="menu"가 적합합니다. 닫힘 전 애니메이션 동안 스크린 리더에 노출을 막기 위해 aria-hidden 처리도 권장합니다.

-    <ul 
+    <ul 
+      role="menu"
+      aria-hidden={isClosing}
       className={cn(
         styles.dropdownContent,
         isClosing? styles.dropdownContentClosing: '',
         className
       )}
     >
shared/types/api/letter.ts (1)

35-35: objectKey 필드 optional 처리 및 빈 문자열 전송 방지 제안

  • 기존 objectKeys 잔여 참조는 검색 결과 없으므로 모두 정리된 상태입니다.
  • 다만 objectKey: "" 패턴이 2곳에서 발견되었습니다.
    • app/(sub)/capsule-detail/_components/write-modal/index.tsx:51
    • app/(sub)/capsule-detail/_components/write-modal/index.tsx:77
  • 서버 계약이 “파일 미첨부 시 해당 필드 생략”이라면, 빈 문자열("") 전송은 400 에러나 서버 에러를 유발할 수 있습니다.
  • 안전한 처리 방안
    1. 인터페이스를 optional로 변경
      export interface WriteLetterReq {
        capsuleId: string;
        content: string;
      - objectKey: string;
      + objectKey?: string;
        from: string;
      }
    2. 전송 로직에서 값 존재 시에만 포함
      const payload: WriteLetterReq = {
        capsuleId,
        content,
        from,
        ...(objectKey && { objectKey }),
      };
    또는 폼 기본값을 undefined로 두어, 빈 문자열 대신 undefined가 전송되도록 수정해주세요.
app/(sub)/capsule-detail/_components/write-modal/write-modal.css.ts (1)

72-75: title 버튼에 flex 적용은 OK. 단, disabled 상태 hover 차단 및 포커스 가시성 보완 필요

버튼에 display: "flex"로 중앙 정렬된 것은 적절합니다. 다만 현재 :hover 스타일이 있어 비활성화(disabled) 시에도 시각적 피드백이 남을 수 있고, 키보드 접근성을 위한 :focus-visible 스타일이 없습니다. 아래와 같이 보완해 주세요.

 export const title = style({
   display: "flex",
   alignItems: "center",
   justifyContent: "center",
   ...themeVars.text.B1,
   color: themeVars.color.purple[10],
   margin: 0,

   ":hover": {
     background: themeVars.color.black["80"],
   },

   padding: "1rem 1.2rem",
   borderRadius: "8px",
   transition: "all 0.3s ease",
   zIndex: 1,
   selectors: {
-    "&:disabled": {
+    "&:disabled": {
       color: themeVars.color.white[30],
       cursor: "not-allowed",
     },
+    // disabled 상태에서는 hover 효과 제거
+    "&:disabled:hover": {
+      background: "transparent",
+    },
+    // 키보드 포커스 가시성
+    "&:focus-visible": {
+      outline: `2px solid ${themeVars.color.purple[10]}`,
+      outlineOffset: "2px",
+    },
   },
 });
app/(sub)/capsule-detail/_components/write-modal/index.tsx (4)

47-53: defaultValues의 objectKey 빈 문자열 초기값 재검토

파일 미첨부가 기본 시나리오라면 objectKey""를 넣는 대신 undefined로 두는 편이 안전합니다. 서버가 빈 문자열을 유효값으로 처리하지 않으면 불필요한 검증 실패가 날 수 있습니다. 타입을 optional로 전환(파일: shared/types/api/letter.ts)하고, 초기값에서 제외하거나 undefined로 두는 것을 권장합니다.

  const {
    handleSubmit,
    setValue,
    getValues,
    control,
    reset,
    formState: { errors },
  } = useForm<WriteLetterReq>({
    defaultValues: {
      capsuleId: capsuleData.result.id.toString(),
      content: "",
      from: "",
-     objectKey: "",
+     // 파일 미첨부 기본값: 포함하지 않음(또는 undefined)
+     // objectKey: undefined,
    },
  });

71-84: 모달 오픈 시 폼 초기화 useEffect 의존성 주의

[isOpen]만 의존하면 의도대로 “열릴 때마다 초기화”는 되지만, ESLint(react-hooks/exhaustive-deps) 경고가 발생할 수 있습니다. 경고를 허용하지 않는 설정이라면 다음 두 가지 중 택1을 권장합니다.

  • 최소한 reset, capsuleData.result.id, removeImage는 안정적으로 변하지 않더라도 의존성에 포함.
  • 또는 규칙을 이 위치에서만 명시적으로 무시.
-  }, [isOpen]);
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [isOpen]);

또는:

-  }, [isOpen]);
+  }, [isOpen, reset, removeImage, capsuleData.result.id]);

두 번째 방법은 uploadedImageUrl까지 넣으면 업로드 상태 변화에 의해 불필요한 초기화가 발생할 수 있으니 포함하지 마세요.


124-129: 제출 버튼 대기 상태 접근성 보완(aria 속성 추가 권장)

스피너 렌더링은 좋습니다. 스크린리더를 위해 busy 상태를 명시하면 더 견고해집니다.

-  <button 
-    type="submit" 
-    className={styles.title} 
-    disabled={(isPending || isUploading)}
-  >
-    {(isPending || isUploading) ? <PulseLoader color="#FFFFFF" size={5}/> : '편지담기'}
-  </button>
+  <button
+    type="submit"
+    className={styles.title}
+    disabled={(isPending || isUploading)}
+    aria-busy={(isPending || isUploading)}
+    aria-disabled={(isPending || isUploading)}
+  >
+    {(isPending || isUploading)
+      ? <PulseLoader color="#FFFFFF" size={5} />
+      : '편지담기'}
+  </button>

73-78: reset 시 objectKey 빈 문자열 초기화 동일 이슈

위 defaultValues와 동일하게, 파일 미첨부 시에는 objectKey를 생략하거나 undefined로 초기화하는 것이 안전합니다. 서버 스펙에 따라 에러 가능성이 있습니다.

       reset({
         capsuleId: capsuleData.result.id.toString(),
         content: "",
         from: "",
-        objectKey: "",
+        // objectKey: undefined,
       });
shared/api/mutations/capsule.ts (3)

12-22: 생성 후 invalidateQueries(all) 사용: 안전하지만 과도한 재요청 가능

capsuleQueryKeys.all() 전체 무효화는 간단하고 안전하지만, 리스트/상세/검색 등 모든 화면에서 불필요한 재요청이 발생할 수 있습니다. 생성 직후 필요한 키만 타겟팅하거나, 완료 화면으로의 내비게이션 타이밍에 맞춰 필요한 쿼리만 무효화하는 쪽이 더 효율적입니다.

- onSuccess: () => {
-   queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.all() });
- },
+ onSuccess: () => {
+   // 예: 내 캡슐 목록과 탐색 리스트만 무효화
+   queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.lists() });
+   queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.my() });
+ },

필요 시 상세 키 무효화도 상황에 맞게 추가하세요.


35-37: 좋아요 토글에서 전체 무효화 대신 낙관적 업데이트/타겟 무효화 고려

좋아요 토글은 사용자 체감 성능이 중요한 액션입니다. 전체 무효화는 화면 깜빡임과 과도한 네트워크를 유발할 수 있어, 낙관적 업데이트 또는 현재 화면 컨텍스트의 키만 무효화하는 방식을 권장합니다.

낙관적 업데이트 예시(요지):

  • onMutate에서 캐시 스냅샷 저장 및 즉시 토글
  • onError에서 롤백
  • onSettled에서 타겟 키만 무효화

원하시면 현재 리스트/상세 데이터 구조에 맞춘 구체 코드를 제안드리겠습니다.


44-49: 캡슐 탈퇴 후 전체 무효화도 동일 고려사항

탈퇴 시 리스트/마이 목록 영향만 명확하다면 해당 키만 무효화하는 것이 효율적입니다. 반대로 글로벌 갱신이 필요하다면 현재 방식 유지도 무방합니다(안전성 우선).

- onSuccess: () => {
-   queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.all() });
- },
+ onSuccess: () => {
+   queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.my() });
+   queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.lists() });
+ },
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f63e6b0 and d3a27fd.

📒 Files selected for processing (17)
  • app/(main)/explore/_components/card-container/card-container.css.ts (2 hunks)
  • app/(main)/my-capsule/_components/card-container/card-container.css.ts (2 hunks)
  • app/(sub)/capsule-detail/_components/modal/index.tsx (0 hunks)
  • app/(sub)/capsule-detail/_components/write-modal/index.tsx (5 hunks)
  • app/(sub)/capsule-detail/_components/write-modal/write-modal.css.ts (1 hunks)
  • app/(sub)/create-capsule/_components/steps/intro-step/index.tsx (2 hunks)
  • app/(sub)/create-capsule/page.tsx (3 hunks)
  • app/(sub)/search/_components/card-container/card-container.css.ts (2 hunks)
  • shared/api/mutations/capsule.ts (2 hunks)
  • shared/types/api/letter.ts (1 hunks)
  • shared/ui/chip/chip.stories.tsx (1 hunks)
  • shared/ui/chip/index.tsx (1 hunks)
  • shared/ui/dropdown/dropdown.css.ts (2 hunks)
  • shared/ui/dropdown/index.tsx (3 hunks)
  • shared/ui/popup/popup-cancel-creation/index.tsx (1 hunks)
  • shared/ui/popup/popup-cancel-creation/popup-cancel-creation.css.ts (1 hunks)
  • shared/ui/popup/popup-confirm-letter/popup-confirm-letter.css.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • app/(sub)/capsule-detail/_components/modal/index.tsx
  • shared/ui/popup/popup-confirm-letter/popup-confirm-letter.css.ts
🧰 Additional context used
🧬 Code graph analysis (5)
app/(sub)/create-capsule/page.tsx (1)
shared/hooks/use-overlay.ts (1)
  • useOverlay (7-46)
shared/ui/chip/index.tsx (2)
shared/ui/chip/chip.css.ts (2)
  • chipVariant (16-25)
  • chipBase (5-14)
shared/utils/cn.ts (1)
  • cn (3-5)
shared/api/mutations/capsule.ts (4)
shared/types/api/capsule.ts (2)
  • CreateCapsuleReq (1-7)
  • CreateCapsuleRes (9-14)
shared/api/api-client.ts (1)
  • apiClient (50-65)
shared/constants/endpoints.ts (1)
  • ENDPOINTS (3-41)
shared/api/queries/capsule.ts (1)
  • capsuleQueryKeys (14-34)
shared/ui/popup/popup-cancel-creation/popup-cancel-creation.css.ts (3)
shared/ui/popup/popup-warning-capsule/popup-warning-capsule.css.ts (1)
  • continueButton (8-10)
shared/ui/popup/popup-warning-letter/popup-warning-letter.css.ts (1)
  • continueButton (8-10)
shared/ui/popup/popup.css.ts (1)
  • content (27-34)
shared/ui/dropdown/index.tsx (1)
shared/utils/cn.ts (1)
  • cn (3-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test
  • GitHub Check: deploy
  • GitHub Check: storybook-deploy
🔇 Additional comments (11)
app/(main)/my-capsule/_components/card-container/card-container.css.ts (1)

22-23: 1024px 이상에서 간격 복원 처리 LGTM.

큰 화면에서 카드 밀집도를 완화하는 의도가 명확하고 일관성 있습니다.

app/(main)/explore/_components/card-container/card-container.css.ts (1)

20-21: 1024px 브레이크포인트에서의 간격 재정의 LGTM.

그리드 컬럼 수(4)와 간격을 함께 조정해 가독성/밀도를 균형 있게 맞춥니다.

shared/ui/popup/popup-cancel-creation/index.tsx (1)

15-17: Popup.Content className 전달 및 우선순위 확인 완료
스크립트 결과, Popup.Content 컴포넌트가 최상위 <div>cn(styles.content, className) 형태로 기본 스타일(styles.content) 뒤에 전달된 className을 정상 병합하고 있음을 확인했습니다.
shared/ui/popup/index.tsx 에서

const PopupContent = ({ children, className }: PopupProps) => {
  return <div className={cn(styles.content, className)}>{children}</div>;
};

className 이 최상위 DOM에 그대로 전달됨
shared/ui/popup/popup.css.ts

export const content = style({
  ...themeVars.text.F14,
  /* … */
});

→ 기본 content 스타일은 F14
– 팝업 캔슬 생성 컴포넌트(popup-cancel-creation)에서 import되는 styles.content (F16)는 이후 로드되어 동일한 CSS 특이도 하에 우선 적용

따라서 기본 스타일과 충돌 없이, 의도한 대로 F16이 우선 적용됩니다. 이상 없습니다.

shared/ui/chip/index.tsx (1)

15-18: 클래스 합성 순서 적절 — 소비자 override 보장됨

className={cn(chipBase, chipVariant[variant], className)}에서 사용자 className을 마지막에 둬서 override 가능성이 담보됩니다. 현재 요구사항에 부합하며 유지보수성 측면에서도 깔끔합니다. LGTM.

shared/ui/dropdown/dropdown.css.ts (3)

27-31: wrapper의 borderRadius 적용 위치 확인 필요

dropdownWrapper에 borderRadius를 주셨는데, 실제 라운딩은 dropdownContent에 이미 존재합니다. wrapper에 배경이 없고 overflow도 컨트롤하지 않는다면 시각적 효과가 없을 수 있어요. 의도된 스타일인지 한 번 확인 부탁드립니다.


43-43: 폭 18rem 조정 LGTM

리스트 콘텐츠 길이를 고려한 적절한 너비 조정으로 보입니다.


61-61: 터치 타깃 확장 LGTM

높이를 4.8rem로 늘려 터치 접근성이 개선되었습니다.

shared/ui/dropdown/index.tsx (2)

44-56: 애니메이션 중 토글 가드 도입 LGTM

isAnimating 가드로 중복 토글/레이스 컨디션을 잘 막았습니다.


84-85: 외부 클릭 감지 조건부 활성화 LGTM

닫힘 애니메이션 중 중복 호출을 isAnimating 가드로 막고 있어 안정적입니다.

app/(sub)/capsule-detail/_components/write-modal/index.tsx (2)

65-68: 업로드 콜백 연결은 적절함 — 제거 시 정합성도 보장되는지 확인

onObjectKeyChange로 form state 동기화하는 접근 좋습니다. 이미지 제거 시(removeImage) objectKey도 함께 비우도록 훅에서 보장하는지 확인해 주세요. 훅 내부에서 처리하지 않는다면 여기서도 초기화가 필요합니다.

원하시면 useImageUpload 훅 시그니처/동작을 확인해서 제거 시 setValue("objectKey", undefined)까지 일원화하도록 리팩터 제안 드릴게요.


10-10: 확인 완료: react-spinners 패키지가 dependencies에 추가되어 있습니다 (버전 ^0.17.0). 런타임 모듈 해석 에러는 발생하지 않을 것으로 보입니다.

Comment on lines +78 to +91
// 로딩 화면 2초 동안 지속
useEffect(() => {
if (isPending) {
setShowLoading(true);
} else if (showLoading && !isPending) {
const timer = setTimeout(() => {
setShowLoading(false);
setStep("complete");
}, 2000);

return () => clearTimeout(timer);
}
}, [isPending, showLoading]);

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

에러 시에도 2초 후 강제로 완료 단계로 진행되는 버그

현재 로직은 isPending이 false가 되면(=성공/실패 모두) showLoading이 true인 경우 2초 후 setStep("complete")를 호출합니다. 서버 에러여도 완료 단계로 이동해 빈 화면이 될 수 있습니다(capsuleInfo 없음). 성공/실패를 구분해 타이머를 동작시켜야 합니다.

다음과 같이 isSuccess, isError를 활용해 분기해 주세요.

-  const { mutate: createCapsuleMutate, isPending } = useCreateCapsule();
+  const { mutate: createCapsuleMutate, isPending, isSuccess, isError } = useCreateCapsule();

-  // 로딩 화면 2초 동안 지속
-  useEffect(() => {
-    if (isPending) {
-      setShowLoading(true);
-    } else if (showLoading && !isPending) {
-      const timer = setTimeout(() => {
-        setShowLoading(false);
-        setStep("complete");
-      }, 2000);
-      
-      return () => clearTimeout(timer);
-    }
-  }, [isPending, showLoading]);
+  // 로딩 화면: 요청 중에는 표시, 성공 시에만 2초 유지 후 완료 단계로 이동, 에러 시 해제
+  useEffect(() => {
+    if (isPending) {
+      setShowLoading(true);
+      return;
+    }
+    if (showLoading && isSuccess) {
+      const timer = setTimeout(() => {
+        setShowLoading(false);
+        setStep("complete");
+      }, 2000);
+      return () => clearTimeout(timer);
+    }
+    if (isError) {
+      setShowLoading(false);
+    }
+  }, [isPending, isSuccess, isError, showLoading, setStep]);

또한 onSubmitmutate 옵션에 onError 콜백을 추가해 에러 시 사용자 피드백(토스트/모달)을 제공하는 것을 권장합니다.

📝 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
// 로딩 화면 2초 동안 지속
useEffect(() => {
if (isPending) {
setShowLoading(true);
} else if (showLoading && !isPending) {
const timer = setTimeout(() => {
setShowLoading(false);
setStep("complete");
}, 2000);
return () => clearTimeout(timer);
}
}, [isPending, showLoading]);
// 파일 상단 어딘가에 위치
const { mutate: createCapsuleMutate, isPending, isSuccess, isError } = useCreateCapsule();
// 로딩 화면: 요청 중에는 표시, 성공 시에만 2초 유지 후 완료 단계로 이동, 에러 시 해제
- useEffect(() => {
- if (isPending) {
- setShowLoading(true);
- } else if (showLoading && !isPending) {
- const timer = setTimeout(() => {
- setShowLoading(false);
- setStep("complete");
- }, 2000);
-
- return () => clearTimeout(timer);
- }
useEffect(() => {
if (isPending) {
setShowLoading(true);
return;
}
if (showLoading && isSuccess) {
const timer = setTimeout(() => {
setShowLoading(false);
setStep("complete");
}, 2000);
return () => clearTimeout(timer);
}
if (isError) {
setShowLoading(false);
}
}, [isPending, isSuccess, isError, showLoading, setStep]);

Comment on lines +65 to +83
useEffect(() => {
if (isClosing) {
const timer = setTimeout(() => {
setOpen(false);
setIsClosing(false);
setIsAnimating(false);
}, 150);

return () => clearTimeout(timer);
} else if (open) {
// 열기 애니메이션 완료 후
const timer = setTimeout(() => {
setIsAnimating(false);
}, 150);

return () => clearTimeout(timer);
}
}, [isClosing, open]);

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

열림 애니메이션 타이밍 불일치(200ms vs 150ms) → 눈속임/플리커 가능성

CSS는 slideIn 0.2s인데, 열림 완료 타이머는 150ms입니다. isAnimating이 너무 일찍 해제되어 중간 상태에서 재토글이 가능해지는 등 UX 글리치가 날 수 있습니다. 최소한 200ms로 맞춰주세요. 장기적으로는 onAnimationEnd 기반으로 전환하는 것을 권장합니다.

   } else if (open) {
     // 열기 애니메이션 완료 후
     const timer = setTimeout(() => {
       setIsAnimating(false);
-    }, 150);
+    }, 200);

추가 제안(선택): onAnimationEnd를 활용하면 CSS 변경에도 JS 타이밍을 자동으로 따라갑니다. 필요 시 핸들러 도입 방안을 도와드리겠습니다.

📝 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
useEffect(() => {
if (isClosing) {
const timer = setTimeout(() => {
setOpen(false);
setIsClosing(false);
setIsAnimating(false);
}, 150);
return () => clearTimeout(timer);
} else if (open) {
// 열기 애니메이션 완료 후
const timer = setTimeout(() => {
setIsAnimating(false);
}, 150);
return () => clearTimeout(timer);
}
}, [isClosing, open]);
useEffect(() => {
if (isClosing) {
const timer = setTimeout(() => {
setOpen(false);
setIsClosing(false);
setIsAnimating(false);
}, 150);
return () => clearTimeout(timer);
} else if (open) {
// 열기 애니메이션 완료 후
const timer = setTimeout(() => {
setIsAnimating(false);
}, 200);
return () => clearTimeout(timer);
}
}, [isClosing, open]);
🤖 Prompt for AI Agents
In shared/ui/dropdown/index.tsx around lines 65 to 83, the open-case animation
timeout is set to 150ms while the CSS slideIn animation is 200ms, causing
isAnimating to be cleared too early; change the opening setTimeout delay from
150 to 200ms so isAnimating is toggled after the CSS animation completes
(longer-term consider switching to onAnimationEnd to drive this instead of
hardcoded timeouts).

@github-actions
Copy link
Copy Markdown

This pull request (commit 1bbc759) has been deployed to Vercel ▲ - View GitHub Actions Workflow Logs

Name Link
🌐 Unique https://time-capsule-9a41gjkdy-hs-projects-b4a69d5f.vercel.app
🔍 Inspect https://vercel.com/hs-projects-b4a69d5f/time-capsule/BGqYWeNvx3XKu3q8Mfyc7LKi9oeo

Copy link
Copy Markdown

@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 (5)
shared/ui/dropdown/dropdown.css.ts (5)

5-14: 열림 애니메이션의 스케일 값 완화 제안 (튀는 감 감소)

초기 스케일 0.5 → 1 전환은 시각적으로 과격해 보일 수 있습니다. 미세한 트랜스폼으로도 충분히 역동감을 주면서 가독성을 유지할 수 있어요.

아래처럼 변경을 고려해 주세요:

 const slideIn = keyframes({
   from: {
     opacity: 0,
-    transform: "translateY(-10px) scale(0.5)",
+    transform: "translateY(-6px) scale(0.98)",
   },
   to: {
     opacity: 1,
     transform: "translateY(0) scale(1)",
   },
 });

16-25: 닫힘 애니메이션도 동일한 톤으로 정렬

열림과 닫힘의 트랜지션 감이 다르면 이질적으로 느껴질 수 있습니다. slideIn 수정 시 slideOut도 같은 강도로 맞추는 것을 권장합니다.

 const slideOut = keyframes({
   from: {
     opacity: 1,
     transform: "translateY(0) scale(1)",
   },
   to: {
     opacity: 0,
-    transform: "translateY(-10px) scale(0.5)",
+    transform: "translateY(-6px) scale(0.98)",
   },
 });

38-51: 모션 감소 대응(prefers-reduced-motion) + 페인트 최적화 제안

현재 항상 애니메이션이 적용됩니다. 접근성 관점에서 모션 최소화를 지원하고, 페인트 힌트를 추가하면 미세하게나마 성능에 도움이 됩니다. 또한 width를 18rem로 축소하면서 whiteSpace: "nowrap"이 유지되므로 긴 라벨에서 잘림 가능성이 있습니다(아래 dropdownItem 코멘트에 ellipsis 제안 포함).

 export const dropdownContent = style({
   position: "absolute",
   right: 0,
   top: "100%",
   display: "flex",
   width: "18rem",
   flexDirection: "column",
   backgroundColor: themeVars.color.black[70],
   padding: "0.6rem",
   borderRadius: "15px",
   boxShadow: "0px 4px 16px 0px rgba(0, 0, 0, 0.10)",
-  animation: `${slideIn} 0.15s ease-out`,
+  animation: `${slideIn} 0.15s ease-out`,
   transformOrigin: "top right",
+  willChange: "transform, opacity",
+  "@media": {
+    "(prefers-reduced-motion: reduce)": {
+      animation: "none",
+      transform: "none",
+    },
+  },
 });

58-73: 긴 라벨 처리와 키보드 접근성 보강

  • nowrap이므로 긴 텍스트가 넘칠 수 있어 ellipsis/overflow 추가를 권장합니다.
  • 키보드 탐색 시 :focus-visible에서도 hover와 동일한 피드백이 있으면 접근성이 좋아집니다.
 export const dropdownItem = style({
   display: "flex",
   width: "100%",
   height: "4.8rem",
   padding: "1.6rem 2rem",
   whiteSpace: "nowrap",
   alignItems: "center",
   ...themeVars.text.F16,
   borderRadius: "8px",
   color: themeVars.color.white[100],
+  overflow: "hidden",
+  textOverflow: "ellipsis",
+  outline: "none",
   selectors: {
     "&:hover": {
       backgroundColor: themeVars.color.white[5],
     },
+    "&:focus-visible": {
+      backgroundColor: themeVars.color.white[5],
+    },
   },
 });

참고: height 4.8rem은(루트 10px 기준 48px) 터치 타겟 권장 높이(≥44px)를 만족합니다. 현재 루트 폰트 사이즈 설정에 따라 실제 px 값이 달라질 수 있으니, 디자인 시스템 기준과 일치하는지 한 번만 확인 부탁드립니다.


54-56: Dropdown 닫힘 애니메이션 개선 제안

  • Base + Closing 클래스 적용 확인
    cn(styles.dropdownContent, isClosing ? styles.dropdownContentClosing : '', className) 구문으로 base 클래스가 유지된 채 closing 클래스만 추가되고 있어, 의도한 대로 동작하고 있습니다.

  • 애니메이션 종료 후 상태 전환 처리 추가 검토
    현재 onAnimationEnd 핸들러가 없어 애니메이션 완료 시점에 언마운트나 상태 전환이 이루어지고 있는지 확인이 필요합니다. 임의의 setTimeout 대신 onAnimationEnd로 명시적 처리를 해두면, 타이밍 이슈나 애니메이션 중간 클릭으로 인한 UX 어색함을 방지할 수 있습니다.

  • CSS 개선 제안
    깜빡임 방지(fill-mode), 클릭 차단, transform-origin 고정, reduced-motion 대응을 추가하면 UX가 더욱 견고해집니다:

 export const dropdownContentClosing = style({
-  animation: `${slideOut} 0.15s ease-in`,
+  animation: `${slideOut} 0.15s ease-in`,
+  animationFillMode: "forwards",        // 애니메이션 종료 후 스타일 유지
+  pointerEvents: "none",               // 닫힘 중 클릭 차단
+  transformOrigin: "top right",        // base와 동일한 기준점 고정
+  willChange: "transform, opacity",    // 브라우저 최적화 힌트
+  "@media": {
+    "(prefers-reduced-motion: reduce)": {
+      animation: "none",               // 리듀스 모션 대응
+      opacity: 0,
+    },
+  },
 });
  • Index.tsx 수정 방향
    <ul
      className={cn(
        styles.dropdownContent,
        isClosing ? styles.dropdownContentClosing : "",
        className
      )}
      onAnimationEnd={() => {
        /* 애니메이션 종료 후 상태 변경 로직 */
      }}
    >
    위와 같이 onAnimationEnd를 추가해 언마운트나 상태 플래그 전환을 처리하면, 애니메이션 완전 종료 보장이 가능합니다.
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d3a27fd and 1bbc759.

📒 Files selected for processing (1)
  • shared/ui/dropdown/dropdown.css.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
shared/ui/dropdown/dropdown.css.ts (1)
shared/styles/base/theme.css.ts (1)
  • themeVars (14-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: deploy
🔇 Additional comments (1)
shared/ui/dropdown/dropdown.css.ts (1)

2-2: keyframes 임포트 추가 LGTM

Vanilla Extract의 keyframes 사용을 위한 임포트는 적절합니다. 추가적인 사이드이펙트 없음 확인했습니다.

@seueooo seueooo merged commit d42237f into develop Aug 21, 2025
10 checks passed
@seueooo seueooo deleted the feat/qa/#186 branch August 21, 2025 12:06
Copy link
Copy Markdown
Member

@seung365 seung365 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 👍

if (event.target === event.currentTarget) {
onClose();
}
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

오버레이 클릭시 닫히는 기능 삭제한 이유가 궁금합니다..!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

편지쓰기 모달 안닫히게 해달라고 qa 들어왔어요!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

아하 LetterDetailModal에서도 쓰고 있어서.. 요거 props로 받도록 해보겠슴다

queryKey: capsuleQueryKeys.detail(id),
});
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: capsuleQueryKeys.all() });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

요거 id에서 all로 무효화 하게 된 이유가 궁금합니당

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

detail, my, lists 모두 무효화가 필요해서, 그냥 한번에 all로 해줬습니당

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.

[Feature]: 나머지 QA 반영

2 participants