Skip to content

Comments

[NUGUDI-202] 특정 구내식당 상세 정보 연결 #220

Merged
hijjoy merged 5 commits intomainfrom
feature/NUGUDI-202-detail-api
Nov 20, 2025
Merged

[NUGUDI-202] 특정 구내식당 상세 정보 연결 #220
hijjoy merged 5 commits intomainfrom
feature/NUGUDI-202-detail-api

Conversation

@hijjoy
Copy link
Member

@hijjoy hijjoy commented Nov 18, 2025

🌀 Issue Number

😵 As-Is

  1. 구내식당 상세정보가 목데이터로 이루어져 있었어요.
  2. 날짜 데이터가 넘어오고 있는데 영업시간이 없다는 문구로 보였어요.
  3. 탭 부분이 스크롤 되지 않는 이슈가 발생했어요.

🥳 To-Be

  1. 0d92a71 : 해당 부분에서 저는 아직 식단표 부분(구내식당 메뉴 타임라인 조회 (무한 스크롤))을 연결하고싶지 않아 해당 부분은 mock datasource를 바라보도록 처리해두었어요.
  2. 70c1258 : 해당 부분을 처리하면서 비즈니스 로직으로 생성한 메서드(hasPhone 등)를 사용하여 UI 로직에 보여주려고 했으나
  {cafeteria.hasPhone && <InfoRow icon="📞" label={cafeteria.phone!} />}

hasPhone 부분으로 cafeteria.phone이 실제로 존재하는 것을 확인해도 TypeScript는 이 메서드가 true를 반환하는 경우,
해당객체(cafeteria)의 특정 속성(phone)이 자동으로 null 또는 undefined가 아니라는 사실까지는 추론하지 못하는 문제가 생겨 !
연산자를 사용
하였습니다. 이 과정에서 biome 오류가 있어 c67a8a0 을 적용해 두었습니다.

단, 이 부분에 대해서 Adapter에 타입 가드를 추가하는 방법도 생각해 보았는데 오히려 보일러플레이트가 되는 것이 아닌가 해서 앞선
방법으로 적용을 해둔 상태입니다. (hasValidPhone나 hasPhone이나 사실 역할이 동일하기에 엔티티 메소드에서 한번 Adapter에서 한번 두번 정의하는 것이 보일러플레이트라 느낌)

  // Adapter 파일에 추가
  function hasValidPhone(
    item: CafeteriaDetailItem
  ): item is CafeteriaDetailItem & { phone: string } {
    return item.hasPhone; // Entity의 검증 결과 재사용
  }

  // Component
  {hasValidPhone(cafeteria) && <InfoRow label={cafeteria.phone} />}

또는 엔티티 메서드(hasPhone)를 사용하지 않고 직접 속성 체크를 하는 방법도 있지만,

  {cafeteria.phone && <InfoRow icon="📞" label={cafeteria.phone} />}

이는 Entity의 비즈니스 로직(공백 문자열 검증 등)을 우회하게 되어 Clean Architecture의 관심사 분리 원칙에 어긋나므로
지양했습니다. hasPhone 메서드는 단순히 null 체크뿐만 아니라 trim().length > 0 같은 추가 검증 로직을 포함하고 있어, 비즈니스
규칙을 일관되게 적용하기 위해서는 Entity의 메서드를 사용하는 것이 올바르다고 판단했습니다. 하지만 해당 부분에 대해 다른 의견이 있으시면 리뷰 남겨주세요 ! 🙆🏻‍♀️

  1. bd0f62b : 이전 변경사항에서 tab에서 가로스크롤이 나오는 문제를 해결하기 위해 overflowX: "hidden"를 추가했던 이력이 있었습니다. 하지만 해당 변경사항으로 인해 가로스크롤이 생겼던 문제는 아니였고 실제로 overflowX: "hidden" 해당 코드를 삭제하면서 3번 문제를 해결하고, 가로스크롤도 생기지 않았습니다.

✅ Check List

  • 테스트가 전부 통과되었나요?
  • 빌드는 성공했나요?

📷 Screenshot (Optional)

스크린샷 2025-11-18 16 38 49 스크린샷 2025-11-18 16 38 29

👾 Additional Description (Optional)

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 카페테리아 상세 정보 조회 시 로딩 및 오류 상태 처리 개선
  • 버그 수정

    • 탭 컴포넌트 가로 오버플로우 표시 문제 해결
    • 카페테리아 정보 조건부 표시 로직 개선
  • 사용자 경험

    • 식당 영업 시간 및 추가 정보 표시 방식 최적화
    • 카페테리아 상세 페이지 로딩 상태 개선

-  탭 컴포넌트 오버플로우 스타일 제거
- noNonNullAssertion 규칙을 off로 설정
- 비즈니스 로직 메서드로 존재 여부를 검증한 후 non-null assertion 사용하는 경우로 인해 off로 설정
- Query Hook 패턴 적용하여 컴포넌트에서 데이터 페칭 로직 분리
- Suspense와 ErrorBoundary 적용
- 영업시간 문자열 파싱 지원 (API 응답 형식 \"11:30:00\" 처리)
- 시간 파싱 유틸 함수 추가 (parseHour, parseMinute, parseSecond)
- 영업시간 비고를 별도 줄로 분리하여 모바일 UI 개선
- 불필요한 acceptsMealTicket 비즈니스 로직 제거
- 비즈니스 로직 메서드 일관성 적용 (canTakeout, hasPhone, hasMealTicketPrice)
@hijjoy hijjoy self-assigned this Nov 18, 2025
@hijjoy hijjoy linked an issue Nov 18, 2025 that may be closed by this pull request
4 tasks
@vercel
Copy link
Contributor

vercel bot commented Nov 18, 2025

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

Project Deployment Preview Comments Updated (UTC)
nugudi Ready Ready Preview Comment Nov 18, 2025 0:44am

@github-actions
Copy link

github-actions bot commented Nov 18, 2025

Jira Issue: NUGUDI-202

  • 제목: 특정 구내식당 상세 정보 연결
  • 상태: To Do
  • 담당자: hyewon

@gemini-code-assist
Copy link

Summary of Changes

Hello @hijjoy, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 구내식당 상세 정보 페이지의 데이터 연동 및 사용자 경험 개선에 중점을 둡니다. 목데이터 사용을 줄이고 실제 데이터 흐름을 구축하며, 영업시간 데이터 처리의 정확성을 높였습니다. 또한, UI 컴포넌트의 렌더링 로직을 최적화하고, 데이터 로딩 및 에러 처리를 위한 React의 Suspense와 ErrorBoundary 패턴을 도입하여 애플리케이션의 안정성과 반응성을 향상시켰습니다.

Highlights

  • 구내식당 상세 정보 연결: 구내식당 상세 정보가 목데이터 대신 실제 데이터 소스에 연결되도록 변경되었습니다. 이를 위해 데이터 매퍼와 DI 컨테이너가 업데이트되었습니다.
  • 영업시간 데이터 처리 개선: 영업시간(Business Hours) DTO가 문자열 형식('HH:MM:SS' 또는 'HH:MM')으로 넘어올 경우를 대비하여 시, 분, 초를 파싱하는 유틸리티 함수(parseHour, parseMinute, parseSecond)가 추가되었고, 이를 통해 영업시간 정보가 정확하게 표시됩니다.
  • 탭 스크롤 이슈 해결: 이전 변경사항에서 탭 컴포넌트의 가로 스크롤 문제를 해결하기 위해 추가되었던 overflowX: "hidden" 속성이 제거되었습니다. 이 속성이 실제 문제의 원인이 아니었으며, 제거 후에도 가로 스크롤이 발생하지 않고 탭 스크롤 문제가 해결되었습니다.
  • 메뉴 타임라인 Mock 데이터 유지: 백엔드 API가 아직 완성되지 않은 getCafeteriaMenuTimeline 기능은 임시적으로 Mock 데이터 소스를 계속 사용하도록 명시적으로 처리되었습니다.
  • UI 로직 및 타입 안전성 개선: 구내식당 엔티티에 hasNote() 메서드가 추가되어 영업시간 비고 정보 유무를 확인할 수 있게 되었고, CafeteriaDetailItem 타입 및 관련 UI 컴포넌트들이 업데이트되었습니다. 또한, TypeScript의 타입 추론 한계로 인해 ! 연산자를 사용해야 하는 경우를 위해 Biome 설정에서 noNonNullAssertion 규칙이 일시적으로 비활성화되었습니다.
  • Suspense 및 ErrorBoundary 적용: 구내식당 상세 페이지의 CafeteriaHeroSectionSuspenseErrorBoundary가 적용되어 데이터 로딩 중 스켈레톤 UI를 표시하고, 에러 발생 시 폴백 UI를 보여주는 등 사용자 경험이 개선되었습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이 PR은 구내식당 상세 정보 페이지를 목 데이터에서 실제 API와 연결하는 중요한 작업을 성공적으로 수행했습니다. useSuspenseQuery를 사용한 커스텀 훅을 도입하여 데이터 로딩 상태를 선언적으로 처리한 점이 인상적입니다. 또한, 코드 구조가 클린 아키텍처 원칙을 잘 따르고 있으며, non-null assertion(!) 사용에 대한 이유를 PR 설명에 상세히 기술해주셔서 리뷰에 큰 도움이 되었습니다. 전반적으로 훌륭한 변경 사항입니다.

Comment on lines +44 to +54
{cafeteria.hasNote && (
<InfoRow icon="📝" label={cafeteria.businessHours!.note!} />
)}
{cafeteria.hasMealTicketPrice && (
<InfoRow
icon="💰"
label={`가격 ${formatPriceWithCurrency(cafeteria.mealTicketPrice!)}`}
/>
)}
{cafeteria.canTakeout && <InfoRow icon="📦" label="포장 가능" />}
{cafeteria.hasPhone && <InfoRow icon="📞" label={cafeteria.phone!} />}

Choose a reason for hiding this comment

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

high

PR 설명에서 언급해주신 것처럼, has... 프로퍼티로 존재 여부를 확인한 후 non-null assertion(!)을 사용하고 계십니다. biome.json에서 관련 규칙을 비활성화한 것은 팀의 결정으로 보이지만, 이 패턴은 has... 로직과 실제 프로퍼티의 상태가 일치하지 않을 경우 런타임 에러를 발생시킬 수 있는 위험을 내포하고 있습니다.

PR 설명에서 직접 제안해주신 타입 가드(type guard)를 사용하는 것이 훨씬 안전한 방법입니다. 타입 가드를 사용하면 컴파일 타임에 타입 안전성을 보장받을 수 있으며, ! 연산자를 사용하지 않아도 됩니다.

예를 들어, 다음과 같은 타입 가드를 정의하여 사용할 수 있습니다.

// 타입 가드 정의
function hasNote(cafeteria: CafeteriaDetailItem): cafeteria is CafeteriaDetailItem & { businessHours: BusinessHoursItem & { note: string } } {
  return cafeteria.hasNote;
}

function hasPhone(cafeteria: CafeteriaDetailItem): cafeteria is CafeteriaDetailItem & { phone: string } {
  return cafeteria.hasPhone;
}
        {hasNote(cafeteria) && (
          <InfoRow icon="📝" label={cafeteria.businessHours.note} />
        )}
        {cafeteria.hasMealTicketPrice && (
          <InfoRow
            icon="💰"
            label={`가격 ${formatPriceWithCurrency(
              cafeteria.mealTicketPrice!,
            )}`}
          />
        )}
        {cafeteria.canTakeout && <InfoRow icon="📦" label="포장 가능" />}
        {hasPhone(cafeteria) && <InfoRow icon="📞" label={cafeteria.phone} />}

Copy link
Member Author

Choose a reason for hiding this comment

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

@dydals3440 해당 부분에 대해 위에 설명을 추가적으로 적어두었습니다 ! 의견주시면 감사하겠습니다 🐿️

@coderabbitai
Copy link

coderabbitai bot commented Nov 18, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

개요

시간 파싱 유틸리티 추가, 카페테리아 데이터 소스를 목(mock)에서 실제 HTTP 기반으로 전환, 도메인 엔티티 메서드 업데이트(hasNote 추가, acceptsMealTicket 제거), UI 컴포넌트 및 어댑터 리팩토링, 카페테리아 상세 조회 쿼리 훅 생성, 탭 컴포넌트 스타일 조정.

변경 사항

응집도 / 파일 요약
시간 파싱 유틸리티
apps/web/src/core/shared/util/date/parse-time.ts, apps/web/src/core/shared/util/date/index.ts
parseHour, parseMinute, parseSecond 함수 추가 및 index에서 재내보내기
카페테리아 시간 변환 로직
apps/web/src/domains/cafeteria/data/mapper/cafeteria.mapper.ts
"HH:MM:SS" 및 "HH:MM" 형식의 문자열 기반 LocalTime 파싱 지원
데이터 소스 전환
apps/web/src/domains/cafeteria/data/remote/api/cafeteria-remote-data-source-impl.ts, apps/web/src/domains/cafeteria/di/cafeteria-client-container.ts, apps/web/src/domains/cafeteria/di/cafeteria-server-container.ts
CafeteriaRemoteDataSourceMockImpl에서 CafeteriaRemoteDataSourceImpl로 전환, HTTP 클라이언트 및 인증 인프라 추가
도메인 엔티티 업데이트
apps/web/src/domains/cafeteria/domain/entities/business-hours.entity.ts, apps/web/src/domains/cafeteria/domain/entities/cafeteria.entity.ts
hasNote() 메서드 추가, acceptsMealTicket() 메서드 제거
카페테리아 상세 조회 쿼리
apps/web/src/domains/cafeteria/presentation/client/hooks/queries/get-cafeteria-detail.query.ts
useGetCafeteriaDetail 훅 신규 생성
UI 어댑터 및 타입
apps/web/src/domains/cafeteria/presentation/shared/adapters/cafeteria.adapter.ts, apps/web/src/domains/cafeteria/presentation/shared/types/cafeteria.ts
CafeteriaDetailItem에 hasNote 추가, acceptsMealTicket 제거, 비즈니스 시간 포맷팅 수정
UI 컴포넌트 리팩토링
apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-card/index.tsx, apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-tab/index.tsx, apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-tab/index.css.ts, apps/web/src/domains/cafeteria/presentation/shared/ui/sections/cafeteria-hero-section/index.tsx, apps/web/src/domains/cafeteria/presentation/shared/ui/views/cafeteria-detail-view/index.tsx
CafeteriaDetailItem 사용으로 업데이트, 조건부 렌더링 로직 추가, useGetCafeteriaDetail 훅 통합, ErrorBoundary/Suspense 래핑 추가
설정 및 스타일
biome.json, packages/react/components/tab/src/style.css.ts
noNonNullAssertion 린터 규칙 비활성화, 탭 컴포넌트에서 overflowX 제거

시퀀스 다이어그램

sequenceDiagram
    participant UI as Cafeteria Detail View
    participant Hook as useGetCafeteriaDetail
    participant UseCase as GetCafeteriaById
    participant Adapter as CafeteriaAdapter
    
    UI->>Hook: useGetCafeteriaDetail(cafeteriaId)
    activate Hook
    Hook->>UseCase: execute(cafeteriaId)
    activate UseCase
    UseCase-->>Hook: Cafeteria (domain entity)
    deactivate UseCase
    Hook->>Adapter: toUiDetailItem(cafeteria)
    activate Adapter
    Adapter-->>Hook: CafeteriaDetailItem
    deactivate Adapter
    Hook-->>UI: Query with data
    deactivate Hook
    UI->>UI: Render with Suspense/ErrorBoundary
Loading
sequenceDiagram
    participant Old as 이전: Mock 데이터 소스
    participant New as 새로운: HTTP 기반 데이터 소스
    participant DI as DI Container
    participant HTTP as HttpClient (인증)
    
    rect rgb(200, 220, 255)
    Note over Old: 이전 흐름
    DI->>Old: CafeteriaRemoteDataSourceMockImpl 생성
    Old-->>DI: Mock 데이터 반환
    end
    
    rect rgb(200, 255, 220)
    Note over New: 새로운 흐름
    DI->>DI: SessionManager, TokenProvider 생성
    DI->>HTTP: AuthenticatedHttpClient 생성
    DI->>New: CafeteriaRemoteDataSourceImpl 생성 (HttpClient 주입)
    New->>HTTP: API 호출
    HTTP-->>New: 인증된 응답
    New-->>DI: 실제 데이터 반환
    end
Loading

예상 코드 리뷰 노력

🎯 3 (중간 복잡도) | ⏱️ ~25분

추가 검토 필요 영역:

  • DI 컨테이너의 HTTP 인프라 초기화 로직 검증 (SessionManager, TokenProvider, HttpClient 연결)
  • 시간 파싱 유틸리티의 입력 유효성 검사 및 예외 처리 (특히 parseSecond의 undefined 처리)
  • 비즈니스 시간 mapper 리팩토링에서 null 안전성 확인
  • hasNote 메서드 추가에 따른 UI 렌더링 변경 검증 (조건부 렌더링 로직)
  • useGetCafeteriaDetail 훅의 캐싱 정책 (staleTime, gcTime) 적절성 확인
  • ErrorBoundary/Suspense 래핑이 사용자 경험을 개선하는지 검증

관련 PR 목록

추천 레이블

feat, apps/web, packages/react

추천 리뷰어

  • dydals3440

🐰 시간 파서와 HTTP를 만나고,
목 데이터는 안녕히,
실제 API가 춤을 추며,
hasNote 플래그 반짝반짝,
UI는 더욱 똑똑해져라! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 특정 구내식당 상세 정보 연결이라는 주요 변경사항을 명확하게 요약하며, 이슈 번호(NUGUDI-202)와 함께 간결하고 구체적으로 표현되어 있습니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed PR 설명이 대부분 작성되었으나 필수 체크리스트 항목들이 미완료 상태입니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/NUGUDI-202-detail-api

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@github-actions
Copy link

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

Name Link
🌐 Unique https://nugudi-fquay8trn-dydals3440s-projects.vercel.app
🔍 Inspect https://vercel.com/dydals3440s-projects/nugudi/Eu4JLGpwMX34Qixj8yzsn1E2XDX3

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

♻️ Duplicate comments (4)
apps/web/src/domains/cafeteria/data/mapper/cafeteria.mapper.ts (1)

61-61: 타입 안정성을 위해 any 타입을 구체적인 타입으로 변경하는 것을 권장합니다.

이전 리뷰에서도 지적되었듯이, time 파라미터가 any 타입으로 지정되어 있어 타입 검사를 우회하고 있습니다. string | null | undefined와 같이 더 구체적인 타입을 사용하면 타입 안정성이 향상되고 런타임 오류를 방지할 수 있습니다.

다음과 같이 수정할 수 있습니다:

-  const convertLocalTime = (time: any) => {
+  const convertLocalTime = (time: string | null | undefined) => {
apps/web/src/core/shared/util/date/parse-time.ts (1)

54-59: 일관성을 위해 parseSecond도 유효하지 않은 값에 대해 null을 반환하는 것을 고려해보세요.

이전 리뷰에서도 지적되었듯이, parseHourparseMinute는 유효하지 않은 값에 대해 null을 반환하는 반면, parseSecond0을 반환합니다. 이러한 불일치는 파싱 실패를 감지하기 어렵게 만들 수 있습니다. 일관된 인터페이스를 제공하기 위해 null을 반환하도록 변경하는 것을 고려해보세요.

단, 현재 설계가 의도적인 것이라면(초가 선택적이고 기본값이 0), 호출하는 쪽에서 ?? 0을 사용하여 기본값을 명시적으로 설정하는 것이 함수의 재사용성과 예측 가능성을 높일 수 있습니다.

apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-tab/index.tsx (1)

43-55: hasX 플래그 + non-null assertion 패턴은 도메인 인바리언트에 강하게 의존합니다.

hasNote/hasMealTicketPrice/canTakeout/hasPhone 플래그로 존재 여부를 판단한 뒤 businessHours!.note!, mealTicketPrice!, phone!을 사용하는 구조는, 도메인/어댑터가 항상 해당 인바리언트를 지켜준다는 전제하에서는 깔끔하지만 한 번만 어긋나도 런타임 에러가 날 수 있습니다. 팀에서 이미 논의하신 것처럼, 여유가 생기면 공용 타입 가드(예: hasNote(cafeteria), hasPhone(cafeteria))로 이 패턴을 감싸 두면 UI 쪽 타입 안전성이 조금 더 좋아질 것 같습니다.

apps/web/src/domains/cafeteria/di/cafeteria-client-container.ts (1)

140-188: UseCase를 호출 시마다 새로 생성하는 전략은 현재로서는 괜찮아 보이지만, 호출 빈도가 늘면 캐싱도 고려해 볼 수 있습니다.

get...UseCase가 매번 새 인스턴스를 반환하는 구조는 테스트/확장성 면에서는 단순하지만, 이전 리뷰에서 언급된 것처럼 호출이 잦은 경로에서는 불필요한 객체 생성이 누적될 수 있습니다. 현재 트래픽/복잡도 수준에서는 큰 문제는 아니겠지만, 추후 성능 튜닝이 필요해지면 UseCase도 Repository처럼 한 번 생성해 캐싱하는 방향을 옵션으로 열어두시면 좋겠습니다.

🧹 Nitpick comments (4)
apps/web/src/domains/cafeteria/presentation/shared/adapters/cafeteria.adapter.ts (1)

366-371: JSDoc 예제 업데이트 필요

Line 402-403에서 구현이 변경되어 note를 별도로 표시하도록 변경되었으나, JSDoc의 @example (Line 366-367)에는 여전히 note가 포함된 예제가 명시되어 있습니다. 실제 구현과 일치하도록 예제를 업데이트해주세요.

다음과 같이 수정하는 것을 권장합니다:

  * @example
- * // With note
- * "점심: 11:30 ~ 14:00 / 저녁: 17:00 ~ 20:00 (주말 휴무)"
+ * // With lunch and dinner (note is displayed separately)
+ * "점심: 11:30 ~ 14:00 / 저녁: 17:00 ~ 20:00"
apps/web/src/domains/cafeteria/presentation/client/hooks/queries/get-cafeteria-detail.query.ts (1)

14-26: React Query 훅 반환 타입을 명시하면 가독성이 조금 더 좋아질 수 있습니다.

DI/어댑터 구조와 staleTime/gcTime 설정 모두 자연스럽습니다. 다만 useSuspenseQuery의 제네릭을 명시해 두면 IDE에서 반환 타입이 더 명확해지고, React Query 버전에 따라 추론 방식이 달라져도 안전합니다. (이미 queryFn 기반으로 잘 추론되고 있다면 그대로 두셔도 무방합니다.)

예시:

import type { CafeteriaDetailItem } from "@cafeteria/presentation/shared/types";

export function useGetCafeteriaDetail(cafeteriaId: string) {
  const container = getCafeteriaClientContainer();
  const getCafeteriaByIdUseCase = container.getGetCafeteriaById();

  return useSuspenseQuery<CafeteriaDetailItem>({
    queryKey: cafeteriaKeys.detail(cafeteriaId),
    queryFn: async () => {
      const entity = await getCafeteriaByIdUseCase.execute(cafeteriaId);
      return CafeteriaAdapter.toUiDetailItem(entity);
    },
    staleTime: 30 * 60 * 1000,
    gcTime: 60 * 60 * 1000,
  });
}

React Query 현재 사용 중인 버전에서 useSuspenseQuery의 제네릭 추론 동작이 어떻게 정의되어 있는지 한 번만 공식 문서로 확인해 보시면 좋겠습니다.

apps/web/src/domains/cafeteria/domain/entities/business-hours.entity.ts (1)

47-51: hasNote 구현에서 공백 문자열 처리 여부를 한 번만 확인해 주세요.

현재는 this._note !== null만 검사하고 있어서, 만약 백엔드에서 " " 같은 공백 문자열을 내려줄 가능성이 있다면 hasNotetrue지만 UI에는 내용 없는 줄이 표시될 수 있습니다. 그런 케이스를 방지하려면 아래처럼 trim 기준으로 체크하는 쪽이 도메인 인바리언트를 조금 더 강하게 보장해 줄 것 같습니다.

hasNote(): boolean {
  return typeof this._note === "string" && this._note.trim().length > 0;
}

(실제 API가 null 또는 의미 있는 문자열만 내려준다면 지금 구현도 충분히 괜찮습니다.)

Also applies to: 173-175

apps/web/src/domains/cafeteria/di/cafeteria-client-container.ts (1)

7-11: HTTP/세션 관련 의존성의 lazy 초기화 구성이 잘 되어 있습니다.

ClientSessionManager → ClientTokenProvider → FetchHttpClient → AuthenticatedHttpClient → CafeteriaRemoteDataSourceImpl로 이어지는 계층이 한 컨테이너 인스턴스 내에서 한 번만 생성되도록 잘 묶여 있어서, 클라이언트 환경에서 과도한 객체 생성을 막는 구조로 보입니다. 한 가지 정도만 더 보자면, NEXT_PUBLIC_API_URL이 설정되지 않았을 때 baseUrl이 빈 문자열이 되는데, 이 값이 필수 설정이라면 초기화 시점에 에러를 던지거나 의미 있는 기본값을 두는 것도 운영 상 안전망이 될 수 있습니다.

Also applies to: 37-43, 61-67, 71-105, 106-113

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c9f2e3 and 70c1258.

📒 Files selected for processing (18)
  • apps/web/src/core/shared/util/date/index.ts (1 hunks)
  • apps/web/src/core/shared/util/date/parse-time.ts (1 hunks)
  • apps/web/src/domains/cafeteria/data/mapper/cafeteria.mapper.ts (2 hunks)
  • apps/web/src/domains/cafeteria/data/remote/api/cafeteria-remote-data-source-impl.ts (3 hunks)
  • apps/web/src/domains/cafeteria/di/cafeteria-client-container.ts (4 hunks)
  • apps/web/src/domains/cafeteria/di/cafeteria-server-container.ts (2 hunks)
  • apps/web/src/domains/cafeteria/domain/entities/business-hours.entity.ts (2 hunks)
  • apps/web/src/domains/cafeteria/domain/entities/cafeteria.entity.ts (1 hunks)
  • apps/web/src/domains/cafeteria/presentation/client/hooks/queries/get-cafeteria-detail.query.ts (1 hunks)
  • apps/web/src/domains/cafeteria/presentation/shared/adapters/cafeteria.adapter.ts (2 hunks)
  • apps/web/src/domains/cafeteria/presentation/shared/types/cafeteria.ts (1 hunks)
  • apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-card/index.tsx (2 hunks)
  • apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-tab/index.css.ts (1 hunks)
  • apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-tab/index.tsx (3 hunks)
  • apps/web/src/domains/cafeteria/presentation/shared/ui/sections/cafeteria-hero-section/index.tsx (2 hunks)
  • apps/web/src/domains/cafeteria/presentation/shared/ui/views/cafeteria-detail-view/index.tsx (2 hunks)
  • biome.json (1 hunks)
  • packages/react/components/tab/src/style.css.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • packages/react/components/tab/src/style.css.ts
🔇 Additional comments (18)
biome.json (1)

34-34: 린터 규칙을 전역적으로 비활성화하기보다는 타입 안전성 문제를 근본적으로 해결하세요.

비-널 단언(non-null assertion)을 피하려는 의도는 좋지만, 린터 규칙을 전역적으로 끄는 것은 코드베이스 전체의 타입 안전성을 훼손합니다. PR 목표에서 설명했듯이 "보일러플레이트 코드 추가"를 피하기 위해 이 결정이 내려졌다면, 더 나은 대안을 고려해야 합니다:

  1. 타입 가드 함수 (Adapter에서): 엔티티 메서드와 함께 타입 가드를 작성하여 TypeScript가 자동으로 타입을 좁힐 수 있도록 설계
  2. satisfies 키워드: TypeScript 4.9+에서 타입 검증을 지원하며 더 간결함
  3. 로컬 비활성화: 정말 필요한 특정 위치에서만 // @ts-expect-error 또는 // biome-ignore 주석 사용

현재 접근 방식은 타입 안전성 검사를 완전히 우회하므로, 향후 null 참조 버그가 런타임에서만 발견될 수 있습니다.

더 적절한 타입 좁히기 패턴을 적용한 대체 방안을 제시해 드릴 수 있습니다. 타입 가드 도입으로 인한 실제 보일러플레이트 코드량이 얼마나 되는지 함께 검토해보시겠어요?

apps/web/src/core/shared/util/date/index.ts (1)

13-13: 새로운 시간 파싱 유틸리티 export가 적절합니다.

세 개의 시간 파싱 함수들을 date 유틸리티 모듈의 공개 API에 추가한 것이 적절하며, 다른 모듈에서 사용할 수 있도록 올바르게 구성되었습니다.

apps/web/src/domains/cafeteria/data/mapper/cafeteria.mapper.ts (2)

6-6: 시간 파싱 유틸리티 import가 적절합니다.

새로운 시간 파싱 함수들을 올바르게 import하여 사용하고 있습니다.


64-76: 문자열 기반 시간 파싱 로직이 잘 구현되었습니다.

"HH:MM:SS" 및 "HH:MM" 형식의 문자열을 적절하게 파싱하고 있으며, 유효성 검증 로직도 올바르게 구현되어 있습니다. parseHour, parseMinute, parseSecond 유틸리티 함수를 활용하여 각 구성 요소를 검증하고, 유효하지 않은 경우 null을 반환하는 것이 적절합니다.

apps/web/src/core/shared/util/date/parse-time.ts (2)

19-23: parseHour 함수 구현이 적절합니다.

0-23 범위의 시간 값을 올바르게 검증하고 있으며, 유효하지 않은 입력에 대해 null을 반환하는 것이 적절합니다.


36-40: parseMinute 함수 구현이 적절합니다.

0-59 범위의 분 값을 올바르게 검증하고 있으며, 유효하지 않은 입력에 대해 null을 반환하는 것이 적절합니다.

apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-tab/index.css.ts (1)

21-23: 변경사항 확인 완료

한글 레이블에 대해 wordBreak: "keep-all"을 적용하여 어색한 줄바꿈을 방지한 것은 적절한 처리입니다.

apps/web/src/domains/cafeteria/presentation/shared/types/cafeteria.ts (1)

67-67: 타입 정의 확인 완료

hasNote 필드 추가가 도메인 엔티티의 변경사항(CafeteriaEntity.hasNote())과 일관성 있게 반영되었습니다.

apps/web/src/domains/cafeteria/presentation/shared/adapters/cafeteria.adapter.ts (1)

176-176: 변경사항 확인 완료

hasNote 필드가 엔티티의 hasNote() 메서드를 통해 올바르게 매핑되었습니다.

apps/web/src/domains/cafeteria/presentation/shared/ui/views/cafeteria-detail-view/index.tsx (1)

19-23: 에러 처리 및 로딩 상태 관리 개선 확인

ErrorBoundarySuspense를 사용하여 hero 섹션의 에러 및 로딩 상태를 적절하게 처리했습니다.

apps/web/src/domains/cafeteria/domain/entities/cafeteria.entity.ts (1)

134-139: 메서드 추가 확인 완료

hasNote() 메서드가 기존 boolean 체크 메서드들(hasBusinessHours, hasPhone 등)과 동일한 패턴으로 구현되었으며, null-safety 처리도 적절합니다.

apps/web/src/domains/cafeteria/data/remote/api/cafeteria-remote-data-source-impl.ts (1)

125-127: 임시 Mock 데이터 사용 확인

PR 목표에 명시된 대로 메뉴 타임라인 조회는 의도적으로 Mock 데이터를 사용하고 있습니다. TODO 주석이 명확하게 작성되어 향후 실제 API로 교체할 계획이 문서화되어 있습니다.

apps/web/src/domains/cafeteria/di/cafeteria-server-container.ts (1)

80-81: 실제 데이터 소스로 마이그레이션 확인

Mock 데이터 소스에서 실제 HTTP 클라이언트 기반의 데이터 소스로 전환되었습니다. 인증된 HTTP 클라이언트 설정이 적절하게 구성되어 있으며, DI 패턴이 올바르게 적용되었습니다.

apps/web/src/domains/cafeteria/presentation/shared/ui/sections/cafeteria-hero-section/index.tsx (2)

3-3: 데이터 페칭 로직 개선 확인

useGetCafeteriaDetail 훅을 사용한 데이터 페칭 방식으로 리팩토링되어 코드가 더욱 선언적이고 유지보수하기 쉬워졌습니다. 상위 뷰에 추가된 Suspense 경계와도 잘 연동됩니다.

Also applies to: 16-16


26-30: 타입 안정성 개선 확인

HeroImageProps 인터페이스를 명시적으로 정의하여 컴포넌트의 타입 안정성과 가독성이 향상되었습니다.

apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-card/index.tsx (1)

13-26: CafeteriaDetailItem으로의 전환과 canTakeout 사용이 도메인 모델과 잘 맞습니다.

상세 화면에서 사용하는 타입을 CafeteriaDetailItem으로 통일한 점과, 포장 여부를 canTakeout 플래그로 분리해 표현한 구조가 명확하고 유지보수에도 좋아 보입니다.

apps/web/src/domains/cafeteria/presentation/shared/ui/components/cafeteria-info-tab/index.tsx (2)

65-77: 위치 정보 처리 로직이 hasLocation 플래그와 잘 정렬되어 있습니다.

hasLocation으로 좌표 존재 여부를 분기하고, 있을 때만 (latitude, longitude)를 렌더링하며 없을 때는 명시적으로 "위치 정보 없음"을 보여주는 UX가 명확하고, 잠재적인 null 접근 문제도 없어 보입니다.


123-124: InfoRow의 텍스트 스타일 분리가 적절합니다.

Bodystyles.infoLabel 클래스를 분리해서 적용한 덕분에 아이콘과 라벨 스타일을 독립적으로 조정하기 쉬워졌고, 재사용 관점에서도 좋은 방향으로 보입니다.

…nse 통합

- CafeteriaTabSection에서 useGetCafeteriaDetail Query Hook 사용
- 같은 쿼리를 사용하는 HeroSection과 TabSection을 단일 Suspense로 감싸 효율성 개선
- Skeleton과 Error 컴포넌트를 별도 파일로 분리 (skeleton.tsx, error.tsx)
@github-actions
Copy link

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

Name Link
🌐 Unique https://nugudi-qq3vfay08-dydals3440s-projects.vercel.app
🔍 Inspect https://vercel.com/dydals3440s-projects/nugudi/GpVKdKJfcfiJfuA2dKDfMwMRUfCW

@hijjoy hijjoy requested a review from dydals3440 November 18, 2025 12:58
Copy link
Contributor

@dydals3440 dydals3440 left a comment

Choose a reason for hiding this comment

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

70c1258 : 해당 부분을 처리하면서 비즈니스 로직으로 생성한 메서드(hasPhone 등)를 사용하여 UI 로직에 보여주려고 했으나

hasPhone 메서드로 검증해도 TypeScript가 phone의 non-null을 추론하지 못해 ! 연산자를 사용하신 부분에 대한 의견입니다.
현재 접근 방식도 실용적이지만, 조금 더 나아가면 DDD에서는 Value Object 패턴도 많이 사용해서 한번 고려는 해볼만 할 것 같습니다!

// domain/value-objects/Phone.ts
class Phone {
  private constructor(private readonly value: string) {}

  static create(phone: string | null): Phone | null {
    if (phone === null || phone.trim().length === 0) {
      return null;
    }
    return new Phone(phone.trim());
  }

  getValue(): string {
    return this.value;
  }
}

// Entity
class CafeteriaDetailEntity {
  constructor(private readonly phone: Phone | null) {}

  hasPhone(): boolean {
    return this.phone !== null;
  }
}

// Adapter
function toCafeteriaDetailItem(entity: CafeteriaDetailEntity) {
  return {
    phone: entity.phone?.getValue() ?? null,
    hasPhone: entity.hasPhone(),
  };
}

// Component - 타입 안전성 자동 확보
{cafeteria.hasPhone && cafeteria.phone && (
  <InfoRow icon="📞" label={cafeteria.phone} />
)}

현재 단계에서는 비즈니스 로직이 이미 검증을 한 상태이니 ! 연산자 방식을 통해 Biome 에러를 임시 해결하는 것도 충분하다고 생각하기는하지만 향후 확장성을 고려하면 VO 패턴도 검토해보면 좋을 것 같아요!

@hijjoy
Copy link
Member Author

hijjoy commented Nov 19, 2025

현재 단계에서는 비즈니스 로직이 이미 검증을 한 상태이니 ! 연산자 방식을 통해 Biome 에러를 임시 해결하는 것도 충분하다고 생각하기는하지만 향후 확장성을 고려하면 VO 패턴도 검토해보면 좋을 것 같아요!

  1. 사실 컴포넌트 레벨에서 아래와 같이 어쩌면 저한테는 같은 의미로 다가오는 cafeteria.hasPhone, cafeteria.phone 두 부분을 둘다 검증하고 싶지 않은 마음이 컸는데요 ! VO 패턴을 사용하더라도 동일한 문제는 생기는 것 같았습니다
{cafeteria.hasPhone && cafeteria.phone && (
  <InfoRow icon="📞" label={cafeteria.phone} />
)}
  1. Discriminated Union을 사용하는 방법도 있지만..
type CafeteriaDetailItem = 
  | { hasPhone: true; phone: string; ... }
  | { hasPhone: false; phone: null; ... };

해당 방법을 사용하면 타입 자체가 정말 너무 길어지고 모든 필드에서 이렇게 작성하는 것은 올바르지 않다고 느꼈습니다.

  1. 만약 null처리 이외의 비즈니스 로직이 있는 경우는 cafeteria.hasPhone && cafeteria.phone 두 검증을 다 하고
    특별한 비즈니스 로직이 없는 경우는 단순히 아래와 같이 하는 것이 좋을까 생각이 들기도합니다. . !
{cafeteria.phone && (
  <InfoRow icon="📞" label={cafeteria.phone} />
)}

일단은 현재방식으로 진행 후에 추후 변경을 하려고 하는데 어떻게 생각하시나용 🐲 @dydals3440

@hijjoy hijjoy merged commit 44ed867 into main Nov 20, 2025
4 checks passed
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.

[NUGUDI-202] 특정 구내식당 상세 정보 연결

2 participants