Skip to content

Comments

Feat: capsule detail 페이지 구현#99

Merged
seueooo merged 10 commits intodevelopfrom
feat/capsule-detail/#95
Jul 27, 2025
Merged

Feat: capsule detail 페이지 구현#99
seueooo merged 10 commits intodevelopfrom
feat/capsule-detail/#95

Conversation

@seueooo
Copy link
Contributor

@seueooo seueooo commented Jul 26, 2025

📌 Summary

📚 Tasks

  • capsule detail 페이지 구현
  • 모션, 반응형
  • 레이아웃 재정비

👀 To Reviewer

이 pr 머지되고 편지쓰기 페이지 마저 해주시면 될 것 같습니다!
그리고 저희 오늘 논의한 max넓이 제약에 따른 레이아웃 그룹핑 진행했는데, max800 -> sub로 폴더 네이밍을 수정했어요.. max800은 너무 스타일에 치우친 명명이라 생각되어 메인 제외 나머지 페이지라는 의미로 sub.. 더 좋은 의견 있으신가요..

이미지는 디자인측에서 전달해주면 추후에 추가할 예정입니다!
세세한 로직/구조는 api 연결하면서 수정해야될 것 같아요 ui 구현만 진행했습니다

📸 Screenshot

2025-07-27.12.14.20.mov

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 캡슐 상세 페이지(좁은 화면용) 및 하위 컴포넌트(캡슐 이미지, 캡션, 정보 타이틀, 오픈 정보, 반응형 푸터) 추가
    • 캡슐 상세 페이지에서 남은 시간, 참여자 수, 오픈일 등 다양한 정보를 시각적으로 확인 가능
    • 링크 복사 및 공유, 편지 담기 등 사용자 인터랙션 버튼 제공
    • 서브 레이아웃 컴포넌트 추가
  • 스타일

    • 각 컴포넌트별로 반응형 및 테마 기반 스타일 추가
    • 레이아웃의 최대 너비 스타일 클래스 도입 및 레이아웃 정렬 방식 개선
    • 일부 텍스트 스타일(기울임 제거) 및 배경/색상/여백 스타일 개선
  • 문서화

    • 캡슐 상세 정보 응답 타입 정의 추가
  • 리팩터

    • 메인 레이아웃 컴포넌트 구조 개선 및 스타일 적용 방식 변경
  • 기타

    • TypeScript 설정(tsconfig.json)에서 typeRoots 옵션 제거

@seueooo seueooo requested a review from seung365 as a code owner July 26, 2025 15:22
@seueooo seueooo linked an issue Jul 26, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Jul 26, 2025

"""

Walkthrough

이번 변경에서는 캡슐 상세 페이지 및 하위 컴포넌트 일체가 신규로 도입되었으며, 전역 스타일 시스템이 개선되고, 레이아웃 컴포넌트 구조가 일부 리팩토링되었습니다. 또한 타입 선언과 tsconfig 설정이 보완되었고, 일부 텍스트 스타일이 수정되었습니다.

Changes

파일/경로 그룹 변경 요약
app/(main)/layout.tsx, app/(sub)/layout.tsx 레이아웃 컴포넌트 화살표 함수 및 CSS 클래스 적용, 구조 리팩토링
app/page.tsx import문 뒤에 공백 한 줄 추가
shared/styles/base/global.css.ts max-width 변수 제거, 명시적 스타일 클래스 추가, 레이아웃 개선
shared/styles/tokens/text.ts B2 텍스트 스타일에서 italic 속성 제거
shared/types/api/capsule.ts CapsuleDetailRes 타입 신규 추가
tsconfig.json typeRoots 설정 제거
app/(sub)/capsule-detail/_components/capsule-image/* CapsuleImage 컴포넌트 및 스타일 신규 추가
app/(sub)/capsule-detail/_components/caption-section/* CaptionSection 컴포넌트 및 스타일 신규 추가
app/(sub)/capsule-detail/_components/info-title/* InfoTitle 컴포넌트 및 스타일 신규 추가
app/(sub)/capsule-detail/_components/open-info-section/* OpenInfoSection 컴포넌트 및 스타일 신규 추가
app/(sub)/capsule-detail/_components/responsive-footer/* ResponsiveFooter 컴포넌트 및 스타일 신규 추가
app/(sub)/capsule-detail/page.tsx, app/(sub)/capsule-detail/page.css.ts 캡슐 상세 페이지 및 스타일 신규 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CapsuleDetailPage
    participant NavbarDetail
    participant InfoTitle
    participant CapsuleImage
    participant CaptionSection
    participant OpenInfoSection
    participant ResponsiveFooter

    User->>CapsuleDetailPage: 페이지 접근
    CapsuleDetailPage->>NavbarDetail: 상단 네비게이션 렌더링
    CapsuleDetailPage->>InfoTitle: 타이틀/참여자수 렌더링
    CapsuleDetailPage->>CapsuleImage: 이미지 렌더링
    CapsuleDetailPage->>CaptionSection: 캡션 렌더링
    CapsuleDetailPage->>OpenInfoSection: 오픈일 정보 렌더링
    CapsuleDetailPage->>ResponsiveFooter: 남은 시간/버튼 렌더링
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20분

Possibly related PRs

Suggested reviewers

  • seung365
    """

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between f27c8a8 and d05c615.

📒 Files selected for processing (4)
  • app/(sub)/capsule-detail/_components/capsule-image/index.tsx (1 hunks)
  • app/(sub)/capsule-detail/_components/caption-section/caption-section.css.ts (1 hunks)
  • app/(sub)/capsule-detail/_components/open-info-section/open-info-section.css.ts (1 hunks)
  • app/(sub)/capsule-detail/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/(sub)/capsule-detail/_components/capsule-image/index.tsx
  • app/(sub)/capsule-detail/page.tsx
  • app/(sub)/capsule-detail/_components/caption-section/caption-section.css.ts
  • app/(sub)/capsule-detail/_components/open-info-section/open-info-section.css.ts
⏰ 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). (1)
  • GitHub Check: test
✨ 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/capsule-detail/#95

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.
    • Explain this complex logic.
    • 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. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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.

Documentation and Community

  • 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.

@github-actions
Copy link

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

Name Link
🌐 Unique https://time-capsule-j2b39n6ca-hs-projects-b4a69d5f.vercel.app
🔍 Inspect https://vercel.com/hs-projects-b4a69d5f/time-capsule/6gZC5Xmh27f5yk974pfN6P7FroZb

@github-actions
Copy link

github-actions bot commented Jul 26, 2025

🚀 Storybook 배포

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

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

🧹 Nitpick comments (7)
shared/types/api/capsule.ts (1)

1-17: 타입 정의를 더 엄격하게 개선해보세요.

인터페이스가 잘 구조화되어 있지만, 다음 개선사항을 고려해보세요:

  1. openAt 필드의 날짜 형식 명시
  2. status 필드를 유니온 타입으로 제한
  3. JSDoc 주석 추가로 문서화 개선
+/**
+ * 캡슐 상세 정보 API 응답 타입
+ */
 export interface CapsuleDetailRes {
   result: {
     id: number;
     title: string;
     subtitle: string;
+    /** ISO 8601 형식의 날짜 문자열 (예: "2024-12-25T00:00:00Z") */
     openAt: string;
     participantCount: number;
     isLiked: boolean;
+    /** 캡슐 상태: 'PENDING' | 'OPENED' | 'EXPIRED' 등 */
-    status: string;
+    status: 'PENDING' | 'OPENED' | 'EXPIRED';
     remainingTime: {
       days: number;
       hours: number;
       minutes: number;
       seconds: number;
     };
   };
 }
shared/styles/base/global.css.ts (1)

5-9: 주석 처리된 코드를 제거해주세요.

사용하지 않는 코드는 주석 대신 완전히 제거하는 것이 코드베이스 유지보수에 더 좋습니다.

-// globalStyle(":root", {
-//   vars: {
-//     "--max-width": "800px",
-//   },
-// });
app/(narrow)/capsule-detail/_components/open-info-section/index.tsx (1)

3-5: Props 인터페이스 개선 제안

openAt 속성의 타입이 너무 generic합니다. 날짜 형식에 대한 더 명확한 타입 정의를 고려해보세요.

interface Props {
-  openAt: string;
+  openAt: string; // ISO 8601 format expected
}

또는 Date 객체나 더 구체적인 날짜 문자열 타입을 사용하는 것이 좋을 것 같습니다.

app/(narrow)/capsule-detail/_components/info-title/index.tsx (1)

2-6: Props 인터페이스 검토

Props 타입 정의가 명확하고 적절합니다. 숫자 타입들이 음수가 될 수 없다면 더 엄격한 타입을 고려해볼 수 있습니다.

선택적으로 더 엄격한 타입 정의를 사용할 수 있습니다:

interface Props {
  title: string;
  participantCount: number; // positive integer
  joinLettersCount: number; // positive integer
}
app/(narrow)/capsule-detail/_components/responsive-footer/index.tsx (2)

53-68: 애니메이션 성능 최적화 고려

무한 반복 애니메이션이 지속적으로 실행되어 성능에 영향을 줄 수 있습니다. 특히 모바일 환경에서 배터리 소모가 우려됩니다.

사용자가 페이지를 보고 있을 때만 애니메이션을 실행하도록 개선을 고려해보세요:

const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
  const handleVisibilityChange = () => {
    setIsVisible(!document.hidden);
  };
  
  document.addEventListener('visibilitychange', handleVisibilityChange);
  setIsVisible(!document.hidden);
  
  return () => {
    document.removeEventListener('visibilitychange', handleVisibilityChange);
  };
}, []);

// motion.div의 animate 속성을 조건부로 변경
animate={isVisible ? { opacity: 0.6, y: -3 } : { opacity: 1, y: 0 }}

52-52: 국제화(i18n) 고려사항

하드코딩된 한국어 텍스트가 있습니다. 향후 다국어 지원을 위해 번역 키 사용을 고려해보세요.

app/(narrow)/capsule-detail/page.tsx (1)

65-91: 애니메이션 성능 최적화

여러 개의 motion.div가 동시에 실행되어 성능에 영향을 줄 수 있습니다. stagger 애니메이션을 사용하여 최적화하는 것을 고려해보세요.

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.4,
    }
  }
};

const itemVariants = {
  hidden: { opacity: 0, y: 10 },
  visible: { opacity: 1, y: 0 }
};

// 사용 예시
<motion.div 
  variants={containerVariants}
  initial="hidden"
  animate="visible"
>
  <motion.div variants={itemVariants}>
    <CaptionSection />
  </motion.div>
  <motion.div variants={itemVariants}>
    <OpenInfoSection />
  </motion.div>
</motion.div>
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9a9b23b and 5085bb0.

⛔ Files ignored due to path filters (1)
  • shared/assets/icon/time.svg is excluded by !**/*.svg
📒 Files selected for processing (19)
  • app/(main)/layout.tsx (1 hunks)
  • app/(narrow)/capsule-detail/_components/capsule-image/capsule-image.css.ts (1 hunks)
  • app/(narrow)/capsule-detail/_components/capsule-image/index.tsx (1 hunks)
  • app/(narrow)/capsule-detail/_components/caption-section/caption-section.css.ts (1 hunks)
  • app/(narrow)/capsule-detail/_components/caption-section/index.tsx (1 hunks)
  • app/(narrow)/capsule-detail/_components/info-title/index.tsx (1 hunks)
  • app/(narrow)/capsule-detail/_components/info-title/info-title.css.ts (1 hunks)
  • app/(narrow)/capsule-detail/_components/open-info-section/index.tsx (1 hunks)
  • app/(narrow)/capsule-detail/_components/open-info-section/open-info-section.css.ts (1 hunks)
  • app/(narrow)/capsule-detail/_components/responsive-footer/index.tsx (1 hunks)
  • app/(narrow)/capsule-detail/_components/responsive-footer/responsive-footer.css.ts (1 hunks)
  • app/(narrow)/capsule-detail/page.css.ts (1 hunks)
  • app/(narrow)/capsule-detail/page.tsx (1 hunks)
  • app/(narrow)/layout.tsx (1 hunks)
  • app/page.tsx (1 hunks)
  • shared/styles/base/global.css.ts (2 hunks)
  • shared/styles/tokens/text.ts (0 hunks)
  • shared/types/api/capsule.ts (1 hunks)
  • tsconfig.json (1 hunks)
💤 Files with no reviewable changes (1)
  • shared/styles/tokens/text.ts
🧰 Additional context used
🧬 Code Graph Analysis (5)
app/(narrow)/layout.tsx (1)
shared/styles/base/global.css.ts (1)
  • maxWidth (45-48)
app/(narrow)/capsule-detail/page.css.ts (5)
app/(narrow)/capsule-detail/_components/caption-section/caption-section.css.ts (1)
  • container (5-18)
app/(narrow)/capsule-detail/_components/capsule-image/capsule-image.css.ts (1)
  • container (4-12)
app/(narrow)/capsule-detail/_components/open-info-section/open-info-section.css.ts (1)
  • container (4-14)
app/(narrow)/capsule-detail/_components/info-title/info-title.css.ts (1)
  • container (5-15)
app/(narrow)/capsule-detail/_components/responsive-footer/responsive-footer.css.ts (1)
  • container (5-20)
app/(main)/layout.tsx (1)
shared/styles/base/global.css.ts (1)
  • mainLayout (50-53)
app/(narrow)/capsule-detail/_components/info-title/index.tsx (1)
app/(narrow)/capsule-detail/_components/info-title/info-title.css.ts (1)
  • title (17-20)
app/(narrow)/capsule-detail/_components/open-info-section/open-info-section.css.ts (2)
app/(narrow)/capsule-detail/_components/caption-section/caption-section.css.ts (1)
  • container (5-18)
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). (3)
  • GitHub Check: storybook-deploy
  • GitHub Check: test
  • GitHub Check: deploy
🔇 Additional comments (12)
app/(narrow)/capsule-detail/page.css.ts (1)

3-8: 컨테이너 스타일 구현이 적절합니다.

다른 컴포넌트들과 일관된 flex 레이아웃 패턴을 따르고 있으며, 1.6rem 간격과 마진 값이 디자인 시스템과 일치합니다.

shared/styles/base/global.css.ts (1)

39-53: 레이아웃 스타일 리팩토링이 적절합니다.

CSS 변수에서 명시적인 클래스로의 변경이 vanilla-extract와 TypeScript 통합에 더 적합합니다. maxWidthmainLayout 클래스가 명확하게 구분되어 있어 좋습니다.

app/(narrow)/capsule-detail/_components/responsive-footer/responsive-footer.css.ts (1)

12-18: iOS Safe-Area와 겹칠 가능성 – 하단 여유 공간을 계산식으로 보완하세요

position: fixed인 상태에서 bottom: "7rem"만 주면 iOS Safari(노치 기기)의 home-indicator 영역을 침범할 수 있습니다. padding 계산에도 동일한 문제가 있습니다.

     position: "fixed",
-    bottom: "7rem",
+    bottom: "calc(7rem + env(safe-area-inset-bottom, 0px))",
 ...
-    padding: "1.2rem 1.2rem 1.2rem 2.4rem",
+    /* 하단 패딩에 safe-area 보정 추가 */
+    padding: "1.2rem 1.2rem calc(1.2rem + env(safe-area-inset-bottom, 0px)) 2.4rem",

이렇게 하면 모든 기기에서 버튼이 가려지지 않습니다.
[ suggest_optional_refactor ]

app/page.tsx (1)

2-2: 불필요한 빈 줄 – 스타일 가이드에 따라 import 이후 공백 한 줄이 의도된 것인지 확인해주세요.
[ skip ]

app/(narrow)/layout.tsx (1)

4-6: 콘텐츠가 좌측 정렬됨 – margin: 0 auto로 중앙 정렬 고려

maxWidth 스타일은 너비만 제한하고 가운데 정렬은 하지 않아, 넓은 화면에서 콘텐츠가 왼쪽에 붙습니다. 중앙 배치가 의도라면 다음처럼 보강해보세요.

-import { maxWidth } from "@/shared/styles/base/global.css";
+import { style } from "@vanilla-extract/css";
+import { maxWidth } from "@/shared/styles/base/global.css";

+const centered = style([maxWidth, { margin: "0 auto" }]);
 ...
-  return <div className={maxWidth}>{children}</div>;
+  return <div className={centered}>{children}</div>;

레이아웃 일관성을 위해 다른 레이아웃 컴포넌트에도 동일 규칙을 적용하는 것이 좋습니다.
[ suggest_optional_refactor ]

app/(narrow)/capsule-detail/_components/capsule-image/index.tsx (1)

5-7: 이미지가 주석 처리되어 빈 컨테이너만 렌더링됩니다

향후 API 연결 시 실제 이미지를 next/image로 교체하는 TODO로 보이지만, 당장은 시각적 공백만 생깁니다.
– 임시 Placeholder라도 넣어 두거나
– PR 설명에 “이미지는 추후 추가 예정”임을 명시하면 리뷰어 혼란을 줄일 수 있습니다.
[ request_verification ]
[ offer_assistance ]
필요하다면 기본 placeholder 컴포넌트를 생성해드릴까요?

app/(narrow)/capsule-detail/_components/caption-section/index.tsx (1)

7-9: 간단 명료, 문제 없음 – 전달받은 description만 그대로 렌더링하며 스타일 분리도 잘 되어 있습니다.
[ approve_code_changes ]

app/(main)/layout.tsx (1)

1-14: 레이아웃 리팩토링 승인

함수 선언에서 화살표 함수로의 변경과 mainLayout CSS 클래스 적용이 잘 되었습니다. 글로벌 스타일 시스템과 일관성 있게 구현되었네요.

app/(narrow)/capsule-detail/_components/caption-section/caption-section.css.ts (1)

5-18: 스타일 구현 검토

반응형 디자인과 테마 변수 활용이 잘 되어있습니다. 다만 배경 그래디언트와 텍스트 색상의 대비율을 확인해보세요.

색상 대비율이 접근성 기준을 만족하는지 확인해주세요:

  • 배경: themeVars.color.gradient.darkgray_op
  • 텍스트: themeVars.color.white[85]

WCAG 2.1 AA 기준(4.5:1 이상)을 충족하는지 검증이 필요합니다.

app/(narrow)/capsule-detail/_components/info-title/index.tsx (1)

12-14: 한국어 텍스트 포맷팅 확인

참여자 수와 편지 수 표시 형식이 적절합니다. 구분자 "﹒"의 사용이 일관성 있는지 다른 컴포넌트들과 확인해보세요.

app/(narrow)/capsule-detail/_components/info-title/info-title.css.ts (1)

5-25: CSS 모듈 구현 승인

스타일 정의가 잘 구조화되어 있습니다:

  • 컨테이너, 제목, 설명의 명확한 분리
  • 테마 변수의 일관된 사용
  • 반응형 패딩 적용이 적절함
  • 색상 위계(white[100] → white[40])가 시각적 구조를 잘 표현
app/(narrow)/capsule-detail/_components/open-info-section/open-info-section.css.ts (1)

1-25: CSS 스타일 구현이 잘 되어 있습니다!

vanilla-extract를 사용한 스타일링이 깔끔하게 구현되었습니다. 테마 변수를 활용하여 일관성을 유지하고 있고, flexbox 레이아웃도 적절합니다.

Comment on lines 4 to 12
export const container = style({
display: "flex",
justifyContent: "center",
alignContent: "center",
margin: "3.2rem",
...screen.md({
margin: "5.6rem",
}),
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

수직 정렬 속성을 수정해주세요.

alignContent 대신 alignItems를 사용하는 것이 적절합니다. alignContent는 여러 줄 flex 컨테이너에서 사용되며, 단일 요소의 수직 정렬에는 alignItems가 더 적합합니다.

 export const container = style({
   display: "flex",
   justifyContent: "center",
-  alignContent: "center",
+  alignItems: "center",
   margin: "3.2rem",
   ...screen.md({
     margin: "5.6rem",
   }),
 });
🤖 Prompt for AI Agents
In app/(narrow)/capsule-detail/_components/capsule-image/capsule-image.css.ts
between lines 4 and 12, replace the CSS property alignContent with alignItems in
the container style object. This change correctly applies vertical alignment for
single-line flex containers by using alignItems instead of alignContent, which
is meant for multi-line flex containers.

const OpenInfoSection = ({ openAt }: Props) => {
return (
<div className={styles.container}>
<TimeIcon className={styles.iconStyle} />
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선 필요

SVG 아이콘에 접근성 속성이 누락되었습니다.

-      <TimeIcon className={styles.iconStyle} />
+      <TimeIcon className={styles.iconStyle} aria-hidden="true" />

시각적 장식용 아이콘이므로 aria-hidden="true"를 추가하는 것이 좋습니다.

📝 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
<TimeIcon className={styles.iconStyle} />
<TimeIcon className={styles.iconStyle} aria-hidden="true" />
🤖 Prompt for AI Agents
In app/(narrow)/capsule-detail/_components/open-info-section/index.tsx at line
10, the TimeIcon SVG lacks accessibility attributes. Since it is a purely
decorative icon, add the attribute aria-hidden="true" to the TimeIcon component
to improve accessibility by hiding it from screen readers.

Comment on lines 19 to 103
const CapsuleDetailPage = () => {
return (
<div>
<NavbarDetail
renderRight={() => {
return (
<>
<LikeButton isLiked={false} />
<Dropdown>
<Dropdown.Trigger>
<MenuIcon />
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Item label="신고하기" />
<Dropdown.Item label="나가기" />
</Dropdown.Content>
</Dropdown>
</>
);
}}
/>
<motion.div
initial={{
opacity: 0,
y: 10,
filter: "blur(10px)",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
>
<InfoTitle
title="비 오는 날의 타임캡슐"
participantCount={8}
joinLettersCount={33}
/>
</motion.div>
<CapsuleImage />
<div className={styles.container}>
<motion.div
initial="hidden"
animate="visible"
variants={fadeUpVariants}
transition={{
type: "spring",
duration: 1.5,
bounce: 0.6,
delay: 0.8,
}}
>
<CaptionSection description="오늘처럼 비 오는 날에만 꺼내보고 싶은 이야기, 혹은 아무에게도 말하지 못했던 감정이 있다면 이곳에 슬며시 적어주세요." />
</motion.div>
<motion.div
initial="hidden"
animate="visible"
variants={fadeUpVariants}
transition={{
type: "spring",
duration: 1.5,
bounce: 0.6,
delay: 1.2,
}}
>
<OpenInfoSection openAt="2025-07-01-13:00" />
</motion.div>
</div>
<ResponsiveFooter
remainingTime={{
days: 2,
hours: 10,
minutes: 10,
}}
/>
</div>
);
};

export default CapsuleDetailPage;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

컴포넌트 구조는 좋으나 데이터 관리 개선 필요

전체적인 컴포넌트 구성과 애니메이션 구현이 잘 되어 있습니다. 하지만 몇 가지 개선할 점이 있습니다.

현재 하드코딩된 데이터를 props나 API에서 가져오도록 변경해야 합니다:

interface CapsuleDetailPageProps {
  capsuleData: {
    title: string;
    participantCount: number;
    joinLettersCount: number;
    description: string;
    openAt: string;
    remainingTime: {
      days: number;
      hours: number;
      minutes: number;
    };
    isLiked: boolean;
  };
}

const CapsuleDetailPage = ({ capsuleData }: CapsuleDetailPageProps) => {
  // 하드코딩된 값들을 capsuleData에서 가져오도록 변경
};
🤖 Prompt for AI Agents
In app/(narrow)/capsule-detail/page.tsx from lines 19 to 103, the component
currently uses hardcoded data for title, participantCount, joinLettersCount,
description, openAt, remainingTime, and isLiked. Refactor the CapsuleDetailPage
component to accept a capsuleData prop with these fields defined in a
CapsuleDetailPageProps interface, and replace all hardcoded values by accessing
the corresponding properties from capsuleData. This will improve data management
and make the component reusable with dynamic data.

Comment on lines 40 to 62
<motion.div
initial={{
opacity: 0,
y: 10,
filter: "blur(10px)",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
>
<InfoTitle
title="비 오는 날의 타임캡슐"
participantCount={8}
joinLettersCount={33}
/>
</motion.div>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성을 위한 motion 설정 고려

사용자가 동작 효과를 비활성화한 경우를 고려해야 합니다.

import { useReducedMotion } from "motion/react";

const CapsuleDetailPage = () => {
  const shouldReduceMotion = useReducedMotion();
  
  const motionProps = shouldReduceMotion ? {} : {
    initial: { opacity: 0, y: 10, filter: "blur(10px)" },
    animate: { opacity: 1, y: 0, filter: "blur(0px)" },
    transition: { type: "spring", stiffness: 100, damping: 20 }
  };

  return (
    <motion.div {...motionProps}>
      {/* content */}
    </motion.div>
  );
};
🤖 Prompt for AI Agents
In app/(narrow)/capsule-detail/page.tsx around lines 40 to 62, the motion.div
animation does not account for users who prefer reduced motion for
accessibility. Import useReducedMotion from "motion/react", then conditionally
apply the motion props: if shouldReduceMotion is true, pass an empty object to
motion.div to disable animations; otherwise, apply the existing animation props.
This ensures the component respects user motion preferences.

@github-actions
Copy link

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

Name Link
🌐 Unique https://time-capsule-84znd53xv-hs-projects-b4a69d5f.vercel.app
🔍 Inspect https://vercel.com/hs-projects-b4a69d5f/time-capsule/4unoT2n6Cn17s77viyEW2SU42fZN

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

🧹 Nitpick comments (6)
app/(sub)/capsule-detail/_components/info-title/index.tsx (1)

2-17: 깔끔한 컴포넌트 구현

TypeScript 인터페이스와 의미있는 HTML 태그 사용이 좋습니다. 접근성 개선을 위해 참가자 정보에 대한 aria-label 추가를 고려해보세요.

<p className={styles.description}>
+  <span aria-label={`${participantCount}명이 참여하고 ${joinLettersCount}개의 편지가 있습니다`}>
    {participantCount}명 참여﹒{joinLettersCount}통
+  </span>
</p>
app/(sub)/capsule-detail/_components/open-info-section/index.tsx (1)

3-15: 날짜 타입 및 스타일링 개선 제안

두 가지 개선사항을 제안합니다:

  1. 날짜 타입을 더 명확하게 정의
  2. 마지막 날짜 표시 요소에 스타일 클래스 추가
interface Props {
-  openAt: string;
+  openAt: string; // ISO date string 또는 formatted date
}

// ...
      <p className={styles.textStyle}>오픈일</p>
-      <p>{openAt}</p>
+      <p className={styles.dateStyle}>{openAt}</p>
app/(sub)/capsule-detail/page.tsx (2)

14-91: 애니메이션 최적화 제안

중복된 애니메이션 설정을 상수로 추출하여 유지보수성을 개선할 수 있습니다.

+const springTransition = {
+  type: "spring" as const,
+  duration: 1.5,
+  bounce: 0.6,
+};

+const staggeredDelays = {
+  caption: 0.8,
+  openInfo: 1.2,
+};

// 사용 시:
transition={{
-  type: "spring",
-  duration: 1.5,
-  bounce: 0.6,
-  delay: 0.8,
+  ...springTransition,
+  delay: staggeredDelays.caption,
}}

32-34: 드롭다운 메뉴 기능 구현 누락

드롭다운 메뉴 아이템들에 클릭 핸들러가 없습니다. 신고하기와 나가기 기능을 구현해야 합니다.

<Dropdown.Content>
-  <Dropdown.Item label="신고하기" />
-  <Dropdown.Item label="나가기" />
+  <Dropdown.Item label="신고하기" onClick={handleReport} />
+  <Dropdown.Item label="나가기" onClick={handleExit} />
</Dropdown.Content>
app/(sub)/capsule-detail/_components/responsive-footer/index.tsx (2)

49-49: 편지 담기 버튼 기능 구현 누락

편지 담기 버튼에 onClick 핸들러가 없습니다. 기능을 구현하거나 props로 받도록 해야 합니다.

-<Button variant="primary" text="편지 담기" />
+<Button variant="primary" text="편지 담기" onClick={onAddLetter} />

53-68: 무한 반복 애니메이션 성능 고려사항

무한 반복 애니메이션이 성능에 영향을 줄 수 있습니다. will-change CSS 속성이나 애니메이션 제한을 고려해보세요.

<motion.div
  initial={{ opacity: 1, y: 0 }}
  animate={{ opacity: 0.6, y: -3 }}
  transition={{
    duration: 0.8,
-    repeat: Number.POSITIVE_INFINITY,
+    repeat: Infinity,
    ease: "easeInOut",
    repeatType: "mirror",
  }}
+  style={{ willChange: "opacity, transform" }}
>
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5085bb0 and f27c8a8.

📒 Files selected for processing (13)
  • app/(sub)/capsule-detail/_components/capsule-image/capsule-image.css.ts (1 hunks)
  • app/(sub)/capsule-detail/_components/capsule-image/index.tsx (1 hunks)
  • app/(sub)/capsule-detail/_components/caption-section/caption-section.css.ts (1 hunks)
  • app/(sub)/capsule-detail/_components/caption-section/index.tsx (1 hunks)
  • app/(sub)/capsule-detail/_components/info-title/index.tsx (1 hunks)
  • app/(sub)/capsule-detail/_components/info-title/info-title.css.ts (1 hunks)
  • app/(sub)/capsule-detail/_components/open-info-section/index.tsx (1 hunks)
  • app/(sub)/capsule-detail/_components/open-info-section/open-info-section.css.ts (1 hunks)
  • app/(sub)/capsule-detail/_components/responsive-footer/index.tsx (1 hunks)
  • app/(sub)/capsule-detail/_components/responsive-footer/responsive-footer.css.ts (1 hunks)
  • app/(sub)/capsule-detail/page.css.ts (1 hunks)
  • app/(sub)/capsule-detail/page.tsx (1 hunks)
  • app/(sub)/layout.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (8)
  • app/(sub)/layout.tsx
  • app/(sub)/capsule-detail/_components/capsule-image/capsule-image.css.ts
  • app/(sub)/capsule-detail/page.css.ts
  • app/(sub)/capsule-detail/_components/caption-section/caption-section.css.ts
  • app/(sub)/capsule-detail/_components/caption-section/index.tsx
  • app/(sub)/capsule-detail/_components/open-info-section/open-info-section.css.ts
  • app/(sub)/capsule-detail/_components/info-title/info-title.css.ts
  • app/(sub)/capsule-detail/_components/responsive-footer/responsive-footer.css.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/(sub)/capsule-detail/_components/info-title/index.tsx (1)
app/(sub)/capsule-detail/_components/info-title/info-title.css.ts (1)
  • title (17-20)
⏰ 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). (1)
  • GitHub Check: test

Comment on lines +23 to +29
const handleClickShareButton = () => {
navigator.clipboard.writeText(window.location.href);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

클립보드 API 에러 처리 필요

클립보드 API 사용 시 에러 처리와 브라우저 지원 확인이 필요합니다.

const handleClickShareButton = async () => {
-  navigator.clipboard.writeText(window.location.href);
-  setIsCopied(true);
-  setTimeout(() => {
-    setIsCopied(false);
-  }, 2000);
+  try {
+    if (!navigator.clipboard) {
+      // 폴백 처리 또는 사용자에게 알림
+      return;
+    }
+    await navigator.clipboard.writeText(window.location.href);
+    setIsCopied(true);
+    setTimeout(() => {
+      setIsCopied(false);
+    }, 2000);
+  } catch (error) {
+    console.error('클립보드 복사 실패:', error);
+    // 사용자에게 에러 알림
+  }
};
🤖 Prompt for AI Agents
In app/(sub)/capsule-detail/_components/responsive-footer/index.tsx around lines
23 to 29, the clipboard API usage lacks error handling and browser support
checks. Wrap the navigator.clipboard.writeText call in a try-catch block to
handle potential errors gracefully. Also, check if navigator.clipboard and
writeText are supported by the browser before attempting to use them, and
provide fallback behavior or user feedback if unsupported or if an error occurs.

Comment on lines 19 to 61
const CapsuleDetailPage = () => {
return (
<div>
<NavbarDetail
renderRight={() => {
return (
<>
<LikeButton isLiked={false} />
<Dropdown>
<Dropdown.Trigger>
<MenuIcon />
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Item label="신고하기" />
<Dropdown.Item label="나가기" />
</Dropdown.Content>
</Dropdown>
</>
);
}}
/>
<motion.div
initial={{
opacity: 0,
y: 10,
filter: "blur(10px)",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
>
<InfoTitle
title="비 오는 날의 타임캡슐"
participantCount={8}
joinLettersCount={33}
/>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

하드코딩된 데이터 제거 필요

컴포넌트 전반에 하드코딩된 데이터가 있습니다. API 연동을 고려하여 props나 상태로 관리하는 것을 권장합니다.

+interface Props {
+  capsuleData: {
+    title: string;
+    participantCount: number;
+    joinLettersCount: number;
+    isLiked: boolean;
+    description: string;
+    openAt: string;
+    remainingTime: {
+      days: number;
+      hours: number;
+      minutes: number;
+    };
+  };
+}

-const CapsuleDetailPage = () => {
+const CapsuleDetailPage = ({ capsuleData }: Props) => {
📝 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
const CapsuleDetailPage = () => {
return (
<div>
<NavbarDetail
renderRight={() => {
return (
<>
<LikeButton isLiked={false} />
<Dropdown>
<Dropdown.Trigger>
<MenuIcon />
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Item label="신고하기" />
<Dropdown.Item label="나가기" />
</Dropdown.Content>
</Dropdown>
</>
);
}}
/>
<motion.div
initial={{
opacity: 0,
y: 10,
filter: "blur(10px)",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
>
<InfoTitle
title="비 오는 날의 타임캡슐"
participantCount={8}
joinLettersCount={33}
/>
interface Props {
capsuleData: {
title: string;
participantCount: number;
joinLettersCount: number;
isLiked: boolean;
description: string;
openAt: string;
remainingTime: {
days: number;
hours: number;
minutes: number;
};
};
}
const CapsuleDetailPage = ({ capsuleData }: Props) => {
return (
<div>
<NavbarDetail
renderRight={() => {
return (
<>
<LikeButton isLiked={false} />
<Dropdown>
<Dropdown.Trigger>
<MenuIcon />
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Item label="신고하기" />
<Dropdown.Item label="나가기" />
</Dropdown.Content>
</Dropdown>
</>
);
}}
/>
<motion.div
initial={{
opacity: 0,
y: 10,
filter: "blur(10px)",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
>
<InfoTitle
title="비 오는 날의 타임캡슐"
participantCount={8}
joinLettersCount={33}
/>
🤖 Prompt for AI Agents
In app/(sub)/capsule-detail/page.tsx between lines 19 and 61, hardcoded data
such as the title, participantCount, and joinLettersCount are used directly in
the component. To fix this, replace these hardcoded values by passing them as
props to the component or managing them through state fetched from an API. This
will make the component dynamic and ready for API integration.

@github-actions
Copy link

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

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

Copy link
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.

sub 네이밍 괜찮은거 같아요~! 고생하셨습니당

@seueooo seueooo merged commit c7e3b87 into develop Jul 27, 2025
8 checks passed
@seueooo seueooo deleted the feat/capsule-detail/#95 branch July 27, 2025 00:48
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]: 캡슐 상세 페이지 구현

2 participants